From e01e4ad5016f806def0b38bd125269dfe3129fc4 Mon Sep 17 00:00:00 2001 From: Darwin Chowdary <39110935+imabhichow@users.noreply.github.com> Date: Tue, 3 Jun 2025 02:24:08 -0700 Subject: [PATCH 1/9] chore(python): examples for searchable encryption --- ..._encrypted_data_key_description_example.py | 82 ++++ .../basic_searchable_encryption_example.py | 312 +++++++++++++ ...on_styles_searchable_encryption_example.py | 298 +++++++++++++ ...nd_beacon_searchable_encryption_example.py | 310 +++++++++++++ ...al_beacon_searchable_encryption_example.py | 415 ++++++++++++++++++ ...est_basic_searchable_encryption_example.py | 40 ++ ...on_styles_searchable_encryption_example.py | 42 ++ ...nd_beacon_searchable_encryption_example.py | 42 ++ ...al_beacon_searchable_encryption_example.py | 43 ++ ..._encrypted_data_key_description_example.py | 27 ++ 10 files changed, 1611 insertions(+) create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/get_encrypted_data_key_description_example.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/basic_searchable_encryption_example.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/beacon_styles_searchable_encryption_example.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/compound_beacon_searchable_encryption_example.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/virtual_beacon_searchable_encryption_example.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/test_basic_searchable_encryption_example.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/test_beacon_styles_searchable_encryption_example.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/test_compound_beacon_searchable_encryption_example.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/test_virtual_beacon_searchable_encryption_example.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/test/test_get_encrypted_data_key_description_example.py diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/get_encrypted_data_key_description_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/get_encrypted_data_key_description_example.py new file mode 100644 index 000000000..f023577f2 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/get_encrypted_data_key_description_example.py @@ -0,0 +1,82 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Example demonstrating how to get encrypted data key descriptions from DynamoDB items.""" + +import boto3 +from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb.client import DynamoDbEncryption +from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb.config import ( + DynamoDbEncryptionConfig, +) +from aws_dbesdk_dynamodb.structures.dynamodb import ( + GetEncryptedDataKeyDescriptionInput, + GetEncryptedDataKeyDescriptionUnionItem, +) + + +def get_encrypted_data_key_description( + table_name: str, + partition_key: str, + partition_key_val: str, + sort_key_name: str, + sort_key_value: str, + expected_key_provider_id: str, + expected_key_provider_info: str, + expected_branch_key_id: str, + expected_branch_key_version: str, +): + """ + Get encrypted data key description from a DynamoDB item. + + :param table_name: The name of the DynamoDB table + :param partition_key: The name of the partition key + :param partition_key_val: The value of the partition key + :param sort_key_name: The name of the sort key + :param sort_key_value: The value of the sort key + :param expected_key_provider_id: The expected key provider ID + :param expected_key_provider_info: The expected key provider info (optional) + :param expected_branch_key_id: The expected branch key ID (optional) + :param expected_branch_key_version: The expected branch key version (optional) + """ + # 1. Create a new AWS SDK DynamoDb client. This client will be used to get item from the DynamoDB table + ddb = boto3.client("dynamodb") + + # 2. Get item from the DynamoDB table. This item will be used to Get Encrypted DataKey Description + key_to_get = {partition_key: {"S": partition_key_val}, sort_key_name: {"N": sort_key_value}} + + response = ddb.get_item(TableName=table_name, Key=key_to_get) + + returned_item = response.get("Item", {}) + if not returned_item: + print(f"No item found with the key {partition_key}!") + return + + # 3. Prepare the input for GetEncryptedDataKeyDescription method. + # This input can be a DynamoDB item or a header. For now, we are giving input as a DynamoDB item + # but users can also extract the header from the attribute "aws_dbe_head" in the DynamoDB table + # and use it for GetEncryptedDataKeyDescription method. + ddb_enc = DynamoDbEncryption(config=DynamoDbEncryptionConfig()) + + input_union = GetEncryptedDataKeyDescriptionUnionItem(returned_item) + + input_obj = GetEncryptedDataKeyDescriptionInput(input=input_union) + + output = ddb_enc.get_encrypted_data_key_description(input=input_obj) + + # In the following code, we are giving input as header instead of a complete DynamoDB item + # This code is provided solely to demo how the alternative approach works. So, it is commented. + + # header_attribute = "aws_dbe_head" + # header = returned_item[header_attribute]["B"] + # input_union = GetEncryptedDataKeyDescriptionUnion( + # header=header + # ) + + # Assert everything + assert output.encrypted_data_key_description_output[0].key_provider_id == expected_key_provider_id + + if expected_key_provider_id.startswith("aws-kms"): + assert output.encrypted_data_key_description_output[0].key_provider_info == expected_key_provider_info + + if output.encrypted_data_key_description_output[0].key_provider_id == "aws-kms-hierarchy": + assert output.encrypted_data_key_description_output[0].branch_key_id == expected_branch_key_id + assert output.encrypted_data_key_description_output[0].branch_key_version == expected_branch_key_version diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/basic_searchable_encryption_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/basic_searchable_encryption_example.py new file mode 100644 index 000000000..0b486e872 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/basic_searchable_encryption_example.py @@ -0,0 +1,312 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Example demonstrating DynamoDB encryption using beacons. + +This example demonstrates how to set up a beacon on an encrypted attribute, +put an item with the beacon, and query against that beacon. +This example follows a use case of a database that stores unit inspection information. + +Running this example requires access to a DDB table with the +following key configuration: + - Partition key is named "work_id" with type (S) + - Sort key is named "inspection_date" with type (S) +This table must have a Global Secondary Index (GSI) configured named "last4-unit-index": + - Partition key is named "aws_dbe_b_inspector_id_last4" with type (S) + - Sort key is named "aws_dbe_b_unit" with type (S) + +In this example for storing unit inspection information, this schema is utilized for the data: + - "work_id" stores a unique identifier for a unit inspection work order (v4 UUID) + - "inspection_date" stores an ISO 8601 date for the inspection (YYYY-MM-DD) + - "inspector_id_last4" stores the last 4 digits of the ID of the inspector performing the work + - "unit" stores a 12-digit serial number for the unit being inspected + +The example requires the following ordered input command line parameters: + 1. DDB table name for table to put/query data from + 2. Branch key ID for a branch key that was previously created in your key store. See the + CreateKeyStoreKeyExample. + 3. Branch key wrapping KMS key ARN for the KMS key used to create the branch key with ID + provided in arg 2 + 4. Branch key DDB table name for the DDB table representing the branch key store +""" +import time +from typing import List + +import boto3 +from aws_cryptographic_material_providers.keystore.client import KeyStore +from aws_cryptographic_material_providers.keystore.config import KeyStoreConfig +from aws_cryptographic_material_providers.keystore.models import KMSConfigurationKmsKeyArn +from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders +from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig +from aws_cryptographic_material_providers.mpl.models import CreateAwsKmsHierarchicalKeyringInput +from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient +from aws_dbesdk_dynamodb.structures.dynamodb import ( + BeaconKeySourceSingle, + BeaconVersion, + DynamoDbTableEncryptionConfig, + DynamoDbTablesEncryptionConfig, + SearchConfig, + SingleKeyStore, + StandardBeacon, +) +from aws_dbesdk_dynamodb.structures.structured_encryption import CryptoAction + +GSI_NAME = "last4-unit-index" + + +def put_item_query_item_with_beacon( + ddb_table_name: str, branch_key_id: str, branch_key_wrapping_kms_key_arn: str, branch_key_ddb_table_name: str +): + """ + Demonstrate using beacons with DynamoDB encryption. + + :param ddb_table_name: The name of the DynamoDB table + :param branch_key_id: Branch key ID for a branch key previously created in key store + :param branch_key_wrapping_kms_key_arn: ARN of KMS key used to create the branch key + :param branch_key_ddb_table_name: Name of DDB table representing the branch key store + """ + # 1. Configure Beacons. + # The beacon name must be the name of a table attribute that will be encrypted. + # The `length` parameter dictates how many bits are in the beacon attribute value. + # The following link provides guidance on choosing a beacon length: + # https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/choosing-beacon-length.html + standard_beacon_list: List[StandardBeacon] = [] + + # The configured DDB table has a GSI on the `aws_dbe_b_inspector_id_last4` AttributeName. + # This field holds the last 4 digits of an inspector ID. + # For our example, this field may range from 0 to 9,999 (10,000 possible values). + # For our example, we assume a full inspector ID is an integer + # ranging from 0 to 99,999,999. We do not assume that the full inspector ID's + # values are uniformly distributed across its range of possible values. + # In many use cases, the prefix of an identifier encodes some information + # about that identifier (e.g. zipcode and SSN prefixes encode geographic + # information), while the suffix does not and is more uniformly distributed. + # We will assume that the inspector ID field matches a similar use case. + # So for this example, we only store and use the last + # 4 digits of the inspector ID, which we assume is uniformly distributed. + # Since the full ID's range is divisible by the range of the last 4 digits, + # then the last 4 digits of the inspector ID are uniformly distributed + # over the range from 0 to 9,999. + # See our documentation for why you should avoid creating beacons over non-uniform distributions + # https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/searchable-encryption.html#are-beacons-right-for-me + # A single inspector ID suffix may be assigned to multiple `work_id`s. + # + # This link provides guidance for choosing a beacon length: + # https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/choosing-beacon-length.html + # We follow the guidance in the link above to determine reasonable bounds + # for the length of a beacon on the last 4 digits of an inspector ID: + # - min: log(sqrt(10,000))/log(2) ~= 6.6, round up to 7 + # - max: log((10,000/2))/log(2) ~= 12.3, round down to 12 + # You will somehow need to round results to a nearby integer. + # We choose to round to the nearest integer; you might consider a different rounding approach. + # Rounding up will return fewer expected "false positives" in queries, + # leading to fewer decrypt calls and better performance, + # but it is easier to identify which beacon values encode distinct plaintexts. + # Rounding down will return more expected "false positives" in queries, + # leading to more decrypt calls and worse performance, + # but it is harder to identify which beacon values encode distinct plaintexts. + # We can choose a beacon length between 7 and 12: + # - Closer to 7, we expect more "false positives" to be returned, + # making it harder to identify which beacon values encode distinct plaintexts, + # but leading to more decrypt calls and worse performance + # - Closer to 12, we expect fewer "false positives" returned in queries, + # leading to fewer decrypt calls and better performance, + # but it is easier to identify which beacon values encode distinct plaintexts. + # As an example, we will choose 10. + # + # Values stored in aws_dbe_b_inspector_id_last4 will be 10 bits long (0x000 - 0x3ff) + # There will be 2^10 = 1,024 possible HMAC values. + # With a sufficiently large number of well-distributed inspector IDs, + # for a particular beacon we expect (10,000/1,024) ~= 9.8 4-digit inspector ID suffixes + # sharing that beacon value. + last4_beacon = StandardBeacon(name="inspector_id_last4", length=10) + standard_beacon_list.append(last4_beacon) + + # The configured DDB table has a GSI on the `aws_dbe_b_unit` AttributeName. + # This field holds a unit serial number. + # For this example, this is a 12-digit integer from 0 to 999,999,999,999 (10^12 possible values). + # We will assume values for this attribute are uniformly distributed across this range. + # A single unit serial number may be assigned to multiple `work_id`s. + # + # This link provides guidance for choosing a beacon length: + # https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/choosing-beacon-length.html + # We follow the guidance in the link above to determine reasonable bounds + # for the length of a beacon on a unit serial number: + # - min: log(sqrt(999,999,999,999))/log(2) ~= 19.9, round up to 20 + # - max: log((999,999,999,999/2))/log(2) ~= 38.9, round up to 39 + # We can choose a beacon length between 20 and 39: + # - Closer to 20, we expect more "false positives" to be returned, + # making it harder to identify which beacon values encode distinct plaintexts, + # but leading to more decrypt calls and worse performance + # - Closer to 39, we expect fewer "false positives" returned in queries, + # leading to fewer decrypt calls and better performance, + # but it is easier to identify which beacon values encode distinct plaintexts. + # As an example, we will choose 30. + # + # Values stored in aws_dbe_b_unit will be 30 bits long (0x00000000 - 0x3fffffff) + # There will be 2^30 = 1,073,741,824 ~= 1.1B possible HMAC values. + # With a sufficiently large number of well-distributed inspector IDs, + # for a particular beacon we expect (10^12/2^30) ~= 931.3 unit serial numbers + # sharing that beacon value. + unit_beacon = StandardBeacon(name="unit", length=30) + standard_beacon_list.append(unit_beacon) + + # 2. Configure Keystore. + # The keystore is a separate DDB table where the client stores encryption and decryption materials. + # In order to configure beacons on the DDB client, you must configure a keystore. + # + # This example expects that you have already set up a KeyStore with a single branch key. + # See the "Create KeyStore Table Example" and "Create KeyStore Key Example" for how to do this. + # After you create a branch key, you should persist its ID for use in this example. + keystore = KeyStore( + config=KeyStoreConfig( + ddb_client=boto3.client("dynamodb"), + ddb_table_name=branch_key_ddb_table_name, + logical_key_store_name=branch_key_ddb_table_name, + kms_client=boto3.client("kms"), + kms_configuration=KMSConfigurationKmsKeyArn(value=branch_key_wrapping_kms_key_arn), + ) + ) + + # 3. Create BeaconVersion. + # The BeaconVersion inside the list holds the list of beacons on the table. + # The BeaconVersion also stores information about the keystore. + # BeaconVersion must be provided: + # - keyStore: The keystore configured in step 2. + # - keySource: A configuration for the key source. + # For simple use cases, we can configure a 'singleKeySource' which + # statically configures a single beaconKey. That is the approach this example takes. + # For use cases where you want to use different beacon keys depending on the data + # (for example if your table holds data for multiple tenants, and you want to use + # a different beacon key per tenant), look into configuring a MultiKeyStore: + # https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/searchable-encryption-multitenant.html + beacon_versions = [ + BeaconVersion( + standard_beacons=standard_beacon_list, + version=1, # MUST be 1 + key_store=keystore, + key_source=BeaconKeySourceSingle( + SingleKeyStore( + # `key_id` references a beacon key. + # For every branch key we create in the keystore, + # we also create a beacon key. + # This beacon key is not the same as the branch key, + # but is created with the same ID as the branch key. + key_id=branch_key_id, + cache_ttl=6000, + ) + ), + ) + ] + + # 4. Create a Hierarchical Keyring + # This is a KMS keyring that utilizes the keystore table. + # This config defines how items are encrypted and decrypted. + # NOTE: You should configure this to use the same keystore as your search config. + mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig()) + + keyring_input = CreateAwsKmsHierarchicalKeyringInput( + branch_key_id=branch_key_id, key_store=keystore, ttl_seconds=6000 + ) + + kms_keyring = mat_prov.create_aws_kms_hierarchical_keyring(input=keyring_input) + + # 5. 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 + # Any attributes that will be used in beacons must be configured as ENCRYPT_AND_SIGN. + attribute_actions = { + "work_id": CryptoAction.SIGN_ONLY, # Our partition attribute must be SIGN_ONLY + "inspection_date": CryptoAction.SIGN_ONLY, # Our sort attribute must be SIGN_ONLY + "inspector_id_last4": CryptoAction.ENCRYPT_AND_SIGN, # Beaconized attributes must be encrypted + "unit": CryptoAction.ENCRYPT_AND_SIGN, # Beaconized attributes must be encrypted + } + + # 6. Create the DynamoDb Encryption configuration for the table we will be writing to. + # The beaconVersions are added to the search configuration. + table_config = DynamoDbTableEncryptionConfig( + logical_table_name=ddb_table_name, + partition_key_name="work_id", + sort_key_name="inspection_date", + attribute_actions_on_encrypt=attribute_actions, + keyring=kms_keyring, + search=SearchConfig(write_version=1, versions=beacon_versions), # MUST be 1 + ) + + table_configs = {ddb_table_name: table_config} + tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) + + # 7. Create the EncryptedClient + ddb_client = boto3.client("dynamodb") + encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config) + + # 8. Put an item into our table using the above client. + # Before the item gets sent to DynamoDb, it will be encrypted + # client-side, according to our configuration. + # Since our configuration includes beacons for `inspector_id_last4` and `unit`, + # the client will add two additional attributes to the item. These attributes will have names + # `aws_dbe_b_inspector_id_last4` and `aws_dbe_b_unit`. Their values will be HMACs + # truncated to as many bits as the beacon's `length` parameter; e.g. + # aws_dbe_b_inspector_id_last4 = truncate(HMAC("4321"), 10) + # aws_dbe_b_unit = truncate(HMAC("123456789012"), 30) + item = { + "work_id": {"S": "1313ba89-5661-41eb-ba6c-cb1b4cb67b2d"}, + "inspection_date": {"S": "2023-06-13"}, + "inspector_id_last4": {"S": "4321"}, + "unit": {"S": "123456789012"}, + } + + put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item) + + assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # 9. Query for the item we just put. + # Note that we are constructing the query as if we were querying on plaintext values. + # However, the DDB encryption client will detect that this attribute name has a beacon configured. + # The client will add the beaconized attribute name and attribute value to the query, + # and transform the query to use the beaconized name and value. + # Internally, the client will query for and receive all items with a matching HMAC value in the beacon field. + # This may include a number of "false positives" with different ciphertext, but the same truncated HMAC. + # e.g. if truncate(HMAC("123456789012"), 30) + # == truncate(HMAC("098765432109"), 30), + # the query will return both items. + # The client will decrypt all returned items to determine which ones have the expected attribute values, + # and only surface items with the correct plaintext to the user. + # This procedure is internal to the client and is abstracted away from the user; + # e.g. the user will only see "123456789012" and never + # "098765432109", though the actual query returned both. + expression_attribute_names = {"#last4": "inspector_id_last4", "#unit": "unit"} + + expression_attribute_values = {":last4": {"S": "4321"}, ":unit": {"S": "123456789012"}} + + # GSIs do not update instantly + # so if the results come back empty + # we retry after a short sleep + for _ in range(10): + query_response = encrypted_ddb_client.query( + TableName=ddb_table_name, + IndexName=GSI_NAME, + KeyConditionExpression="#last4 = :last4 and #unit = :unit", + ExpressionAttributeNames=expression_attribute_names, + ExpressionAttributeValues=expression_attribute_values, + ) + + # Validate query was returned successfully + assert query_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + items = query_response.get("Items", []) + # if no results, sleep and try again + if not items: + time.sleep(0.02) + continue + + # Validate only 1 item was returned: the item we just put + assert len(items) == 1 + returned_item = items[0] + # Validate the item has the expected attributes + assert returned_item["inspector_id_last4"]["S"] == "4321" + assert returned_item["unit"]["S"] == "123456789012" + break diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/beacon_styles_searchable_encryption_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/beacon_styles_searchable_encryption_example.py new file mode 100644 index 000000000..06ea52ea6 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/beacon_styles_searchable_encryption_example.py @@ -0,0 +1,298 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Example demonstrating DynamoDB encryption using beacon styles. + +This example demonstrates how to use Beacons Styles on Standard Beacons on encrypted attributes, + put an item with the beacon, and query against that beacon. +This example follows a use case of a database that stores food information. + This is an extension of the "BasicSearchableEncryptionExample" in this directory + and uses the same table schema. + +Running this example requires access to a DDB table with the +following key configuration: + - Partition key is named "work_id" with type (S) + - Sort key is named "inspection_time" with type (S) + +In this example for storing food information, this schema is utilized for the data: + - "work_id" stores a unique identifier for a unit inspection work order (v4 UUID) + - "inspection_date" stores an ISO 8601 date for the inspection (YYYY-MM-DD) + - "fruit" stores one type of fruit + - "basket" stores a set of types of fruit + - "dessert" stores one type of dessert + - "veggies" stores a set of types of vegetable + - "work_type" stores a unit inspection category + +The example requires the following ordered input command line parameters: + 1. DDB table name for table to put/query data from + 2. Branch key ID for a branch key that was previously created in your key store. See the + CreateKeyStoreKeyExample. + 3. Branch key wrapping KMS key ARN for the KMS key used to create the branch key with ID + provided in arg 2 + 4. Branch key DDB table name for the DDB table representing the branch key store +""" +from typing import List + +import boto3 +from aws_cryptographic_material_providers.keystore.client import KeyStore +from aws_cryptographic_material_providers.keystore.config import KeyStoreConfig +from aws_cryptographic_material_providers.keystore.models import KMSConfigurationKmsKeyArn +from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders +from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig +from aws_cryptographic_material_providers.mpl.models import CreateAwsKmsHierarchicalKeyringInput +from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient +from aws_dbesdk_dynamodb.structures.dynamodb import ( + AsSet, + BeaconKeySourceSingle, + BeaconStyleAsSet, + BeaconStylePartOnly, + BeaconStyleShared, + BeaconStyleSharedSet, + BeaconVersion, + CompoundBeacon, + DynamoDbTableEncryptionConfig, + DynamoDbTablesEncryptionConfig, + EncryptedPart, + PartOnly, + SearchConfig, + Shared, + SharedSet, + SignedPart, + SingleKeyStore, + StandardBeacon, +) +from aws_dbesdk_dynamodb.structures.structured_encryption import CryptoAction + + +def put_item_query_item_with_beacon_styles( + ddb_table_name: str, branch_key_id: str, branch_key_wrapping_kms_key_arn: str, branch_key_ddb_table_name: str +): + """ + Demonstrate using beacon styles with DynamoDB encryption. + + :param ddb_table_name: The name of the DynamoDB table + :param branch_key_id: Branch key ID for a branch key previously created in key store + :param branch_key_wrapping_kms_key_arn: ARN of KMS key used to create the branch key + :param branch_key_ddb_table_name: Name of DDB table representing the branch key store + """ + # 1. Create Beacons. + standard_beacon_list: List[StandardBeacon] = [] + + # The fruit beacon allows searching on the encrypted fruit attribute + # We have selected 30 as an example beacon length, but you should go to + # https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/choosing-beacon-length.html + # when creating your beacons. + fruit_beacon = StandardBeacon(name="fruit", length=30) + standard_beacon_list.append(fruit_beacon) + + # The basket beacon allows searching on the encrypted basket attribute + # basket is used as a Set, and therefore needs a beacon style to reflect that. + # Further, we need to be able to compare the items in basket to the fruit attribute + # so we `share` this beacon with `fruit`. + # Since we need both of these things, we use the SharedSet style. + basket_beacon = StandardBeacon(name="basket", length=30, style=BeaconStyleSharedSet(SharedSet(other="fruit"))) + standard_beacon_list.append(basket_beacon) + + # The dessert beacon allows searching on the encrypted dessert attribute + # We need to be able to compare the dessert attribute to the fruit attribute + # so we `share` this beacon with `fruit`. + dessert_beacon = StandardBeacon(name="dessert", length=30, style=BeaconStyleShared(Shared(other="fruit"))) + standard_beacon_list.append(dessert_beacon) + + # The veggie_beacon allows searching on the encrypted veggies attribute + # veggies is used as a Set, and therefore needs a beacon style to reflect that. + veggie_beacon = StandardBeacon(name="veggies", length=30, style=BeaconStyleAsSet(AsSet())) + standard_beacon_list.append(veggie_beacon) + + # The work_type_beacon allows searching on the encrypted work_type attribute + # We only use it as part of the compound work_unit beacon, + # so we disable its use as a standalone beacon + work_type_beacon = StandardBeacon(name="work_type", length=30, style=BeaconStylePartOnly(PartOnly())) + standard_beacon_list.append(work_type_beacon) + + # Here we build a compound beacon from work_id and work_type + # If we had tried to make a StandardBeacon from work_type, we would have seen an error + # because work_type is "PartOnly" + encrypted_part_list = [EncryptedPart(name="work_type", prefix="T-")] + + signed_part_list = [SignedPart(name="work_id", prefix="I-")] + + compound_beacon_list = [ + CompoundBeacon(name="work_unit", split=".", encrypted=encrypted_part_list, signed=signed_part_list) + ] + + # 2. Configure the Keystore + # These are the same constructions as in the Basic example, which describes these in more detail. + keystore = KeyStore( + config=KeyStoreConfig( + ddb_client=boto3.client("dynamodb"), + ddb_table_name=branch_key_ddb_table_name, + logical_key_store_name=branch_key_ddb_table_name, + kms_client=boto3.client("kms"), + kms_configuration=KMSConfigurationKmsKeyArn(value=branch_key_wrapping_kms_key_arn), + ) + ) + + # 3. Create BeaconVersion. + # This is similar to the Basic example + beacon_versions = [ + BeaconVersion( + standard_beacons=standard_beacon_list, + compound_beacons=compound_beacon_list, + version=1, # MUST be 1 + key_store=keystore, + key_source=BeaconKeySourceSingle(SingleKeyStore(key_id=branch_key_id, cache_ttl=6000)), + ) + ] + + # 4. Create a Hierarchical Keyring + # This is the same configuration as in the Basic example. + mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig()) + + keyring_input = CreateAwsKmsHierarchicalKeyringInput( + branch_key_id=branch_key_id, key_store=keystore, ttl_seconds=6000 + ) + + kms_keyring = mat_prov.create_aws_kms_hierarchical_keyring(input=keyring_input) + + # 5. Configure which attributes are encrypted and/or signed when writing new items. + attribute_actions = { + "work_id": CryptoAction.SIGN_ONLY, # Our partition attribute must be SIGN_ONLY + "inspection_date": CryptoAction.SIGN_ONLY, # Our sort attribute must be SIGN_ONLY + "dessert": CryptoAction.ENCRYPT_AND_SIGN, # Beaconized attributes must be encrypted + "fruit": CryptoAction.ENCRYPT_AND_SIGN, # Beaconized attributes must be encrypted + "basket": CryptoAction.ENCRYPT_AND_SIGN, # Beaconized attributes must be encrypted + "veggies": CryptoAction.ENCRYPT_AND_SIGN, # Beaconized attributes must be encrypted + "work_type": CryptoAction.ENCRYPT_AND_SIGN, # Beaconized attributes must be encrypted + } + + # 6. Create the DynamoDb Encryption configuration for the table we will be writing to. + # The beaconVersions are added to the search configuration. + table_config = DynamoDbTableEncryptionConfig( + logical_table_name=ddb_table_name, + partition_key_name="work_id", + sort_key_name="inspection_date", + attribute_actions_on_encrypt=attribute_actions, + keyring=kms_keyring, + search=SearchConfig(write_version=1, versions=beacon_versions), # MUST be 1 + ) + + table_configs = {ddb_table_name: table_config} + tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) + + # 7. Create the EncryptedClient + ddb_client = boto3.client("dynamodb") + encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config) + + # 8. Create item one, specifically with "dessert != fruit", and "fruit in basket". + item1 = { + "work_id": {"S": "1"}, + "inspection_date": {"S": "2023-06-13"}, + "dessert": {"S": "cake"}, + "fruit": {"S": "banana"}, + "basket": {"SS": ["apple", "banana", "pear"]}, + "veggies": {"SS": ["beans", "carrots", "celery"]}, + "work_type": {"S": "small"}, + } + + # 9. Create item two, specifically with "dessert == fruit", and "fruit not in basket". + item2 = { + "work_id": {"S": "2"}, + "inspection_date": {"S": "2023-06-13"}, + "fruit": {"S": "orange"}, + "dessert": {"S": "orange"}, + "basket": {"SS": ["blackberry", "blueberry", "strawberry"]}, + "veggies": {"SS": ["beans", "carrots", "peas"]}, + "work_type": {"S": "large"}, + } + + # 10. Add the two items + put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item1) + # Validate object put successfully + assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item2) + # Validate object put successfully + assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # 11. Test the first type of Set operation: + # Select records where the basket attribute holds a particular value + expression_attribute_values = {":value": {"S": "banana"}} + + scan_response = encrypted_ddb_client.scan( + TableName=ddb_table_name, + FilterExpression="contains(basket, :value)", + ExpressionAttributeValues=expression_attribute_values, + ) + # Validate query was returned successfully + assert scan_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # Validate only 1 item was returned: item1 + assert len(scan_response["Items"]) == 1 + assert scan_response["Items"][0] == item1 + + # 12. Test the second type of Set operation: + # Select records where the basket attribute holds the fruit attribute + scan_response = encrypted_ddb_client.scan(TableName=ddb_table_name, FilterExpression="contains(basket, fruit)") + # Validate query was returned successfully + assert scan_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # Validate only 1 item was returned: item1 + assert len(scan_response["Items"]) == 1 + assert scan_response["Items"][0] == item1 + + # 13. Test the third type of Set operation: + # Select records where the fruit attribute exists in a particular set + expression_attribute_values = {":value": {"SS": ["boysenberry", "grape", "orange"]}} + + scan_response = encrypted_ddb_client.scan( + TableName=ddb_table_name, + FilterExpression="contains(:value, fruit)", + ExpressionAttributeValues=expression_attribute_values, + ) + # Validate query was returned successfully + assert scan_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # Validate only 1 item was returned: item2 + assert len(scan_response["Items"]) == 1 + assert scan_response["Items"][0] == item2 + + # 14. Test a Shared search. Select records where the dessert attribute matches the fruit attribute + scan_response = encrypted_ddb_client.scan(TableName=ddb_table_name, FilterExpression="dessert = fruit") + # Validate query was returned successfully + assert scan_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # Validate only 1 item was returned: item2 + assert len(scan_response["Items"]) == 1 + assert scan_response["Items"][0] == item2 + + # 15. Test the AsSet attribute 'veggies': + # Select records where the veggies attribute holds a particular value + expression_attribute_values = {":value": {"S": "peas"}} + + scan_response = encrypted_ddb_client.scan( + TableName=ddb_table_name, + FilterExpression="contains(veggies, :value)", + ExpressionAttributeValues=expression_attribute_values, + ) + # Validate query was returned successfully + assert scan_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # Validate only 1 item was returned: item2 + assert len(scan_response["Items"]) == 1 + assert scan_response["Items"][0] == item2 + + # 16. Test the compound beacon 'work_unit': + expression_attribute_values = {":value": {"S": "I-1.T-small"}} + + scan_response = encrypted_ddb_client.scan( + TableName=ddb_table_name, + FilterExpression="work_unit = :value", + ExpressionAttributeValues=expression_attribute_values, + ) + # Validate query was returned successfully + assert scan_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # Validate only 1 item was returned: item1 + assert len(scan_response["Items"]) == 1 + assert scan_response["Items"][0] == item1 diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/compound_beacon_searchable_encryption_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/compound_beacon_searchable_encryption_example.py new file mode 100644 index 000000000..56b10e865 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/compound_beacon_searchable_encryption_example.py @@ -0,0 +1,310 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Example demonstrating DynamoDB encryption using compound beacons. + +This example demonstrates how to set up a compound beacon on encrypted attributes, + put an item with the beacon, and query against that beacon. +This example follows a use case of a database that stores unit inspection information. + This is an extension of the "BasicSearchableEncryptionExample" in this directory. + This example uses the same situation (storing unit inspection information) + and the same table schema. +However, this example uses a different Global Secondary Index (GSI) + that is based on a compound beacon configuration composed of + the `last4` and `unit` attributes. + +Running this example requires access to a DDB table with the +following key configuration: + - Partition key is named "work_id" with type (S) + - Sort key is named "inspection_time" with type (S) +This table must have a Global Secondary Index (GSI) configured named "last4UnitCompound-index": + - Partition key is named "aws_dbe_b_last4UnitCompound" with type (S) + +In this example for storing unit inspection information, this schema is utilized for the data: + - "work_id" stores a unique identifier for a unit inspection work order (v4 UUID) + - "inspection_date" stores an ISO 8601 date for the inspection (YYYY-MM-DD) + - "inspector_id_last4" stores the last 4 digits of the ID of the inspector performing the work + - "unit" stores a 12-digit serial number for the unit being inspected + +The example requires the following ordered input command line parameters: + 1. DDB table name for table to put/query data from + 2. Branch key ID for a branch key that was previously created in your key store. See the + CreateKeyStoreKeyExample. + 3. Branch key wrapping KMS key ARN for the KMS key used to create the branch key with ID + provided in arg 2 + 4. Branch key DDB table name for the DDB table representing the branch key store +""" +import concurrent.futures +import time +from typing import Dict + +import boto3 +from aws_cryptographic_material_providers.keystore.client import KeyStore +from aws_cryptographic_material_providers.keystore.config import KeyStoreConfig +from aws_cryptographic_material_providers.keystore.models import KMSConfigurationKmsKeyArn +from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders +from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig +from aws_cryptographic_material_providers.mpl.models import CreateAwsKmsHierarchicalKeyringInput +from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient +from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_transforms.client import ( + DynamoDbEncryptionTransforms, +) +from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_transforms.models import ( + ResolveAttributesInput, +) +from aws_dbesdk_dynamodb.structures.dynamodb import ( + BeaconKeySourceSingle, + BeaconStylePartOnly, + BeaconVersion, + CompoundBeacon, + Constructor, + ConstructorPart, + DynamoDbTableEncryptionConfig, + DynamoDbTablesEncryptionConfig, + EncryptedPart, + PartOnly, + SearchConfig, + SingleKeyStore, + StandardBeacon, +) +from aws_dbesdk_dynamodb.structures.structured_encryption import CryptoAction + +GSI_NAME = "last4UnitCompound-index" +MAX_CONCURRENT_QUERY_THREADS = 10 + + +def put_and_query_item_with_compound_beacon(ddb_client: EncryptedClient, ddb_table_name: str, item: Dict): + """ + Put and query an item using a compound beacon. + + :param ddb_client: The encrypted DynamoDB client + :param ddb_table_name: The name of the DynamoDB table + :param item: The item to put and query + """ + # Write the item to the table + put_response = ddb_client.put_item(TableName=ddb_table_name, Item=item) + # Validate object put successfully + assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # Query for the item we just put. + expression_attribute_names = {"#compound": "last4UnitCompound"} + + # This query expression takes a few factors into consideration: + # - The configured prefix for the last 4 digits of an inspector ID is "L-"; + # the prefix for the unit is "U-" + # - The configured split character, separating component parts, is "." + # - The default constructor adds encrypted parts in the order they are in the encrypted list, which + # configures `last4` to come before `unit`` + # NOTE: We did not need to create a compound beacon for this query. This query could have also been + # done by querying on the partition and sort key, as was done in the Basic example. + # This is intended to be a simple example to demonstrate how one might set up a compound beacon. + # For examples where compound beacons are required, see the Complex example. + # The most basic extension to this example that would require a compound beacon would add a third + # part to the compound beacon, then query against three parts. + expression_attribute_values = {":value": {"S": "L-5678.U-011899988199"}} + + # GSIs do not update instantly + # so if the results come back empty + # we retry after a short sleep + for _ in range(10): + query_response = ddb_client.query( + TableName=ddb_table_name, + IndexName=GSI_NAME, + KeyConditionExpression="#compound = :value", + ExpressionAttributeNames=expression_attribute_names, + ExpressionAttributeValues=expression_attribute_values, + ) + + # Validate query was returned successfully + assert query_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + items = query_response.get("Items", []) + # if no results, sleep and try again + if not items: + time.sleep(0.02) + continue + + # Validate only 1 item was returned: the item we just put + assert len(items) == 1 + returned_item = items[0] + # Validate the item has the expected attributes + assert returned_item["inspector_id_last4"]["S"] == "5678" + assert returned_item["unit"]["S"] == "011899988199" + break + + +def put_item_query_item_with_compound_beacon( + ddb_table_name: str, branch_key_id: str, branch_key_wrapping_kms_key_arn: str, branch_key_ddb_table_name: str +): + """ + Demonstrate using compound beacons with DynamoDB encryption. + + :param ddb_table_name: The name of the DynamoDB table + :param branch_key_id: Branch key ID for a branch key previously created in key store + :param branch_key_wrapping_kms_key_arn: ARN of KMS key used to create the branch key + :param branch_key_ddb_table_name: Name of DDB table representing the branch key store + """ + # 1. Create Beacons. + # These are the same beacons as in the "BasicSearchableEncryptionExample" in this directory. + # See that file to see details on beacon construction and parameters. + # While we will not directly query against these beacons, + # you must create standard beacons on encrypted fields + # that we wish to use in compound beacons. + # We mark them both as PartOnly to enforce the fact that + # we will not directly query against these beacons. + standard_beacon_list = [ + StandardBeacon(name="inspector_id_last4", length=10, style=BeaconStylePartOnly(PartOnly())), + StandardBeacon(name="unit", length=30, style=BeaconStylePartOnly(PartOnly())), + ] + + # 2. Define encrypted parts. + # Encrypted parts define the beacons that can be used to construct a compound beacon, + # and how the compound beacon prefixes those beacon values. + # A encrypted part must receive: + # - name: Name of a standard beacon + # - prefix: Any string. This is plaintext that prefixes the beaconized value in the compound beacon. + # Prefixes must be unique across the configuration, and must not be a prefix of another prefix; + # i.e. for all configured prefixes, the first N characters of a prefix must not equal another prefix. + # In practice, it is suggested to have a short value distinguishable from other parts served on the prefix. + encrypted_part_list = [ + # For this example, we will choose "L-" as the prefix for "Last 4 digits of inspector ID". + # With this prefix and the standard beacon's bit length definition (10), the beaconized + # version of the inspector ID's last 4 digits will appear as + # `L-000` to `L-3ff` inside a compound beacon. + EncryptedPart(name="inspector_id_last4", prefix="L-"), + # For this example, we will choose "U-" as the prefix for "unit". + # With this prefix and the standard beacon's bit length definition (30), a unit beacon will appear + # as `U-00000000` to `U-3fffffff` inside a compound beacon. + EncryptedPart(name="unit", prefix="U-"), + ] + + constructor_parts = [ + ConstructorPart(name="inspector_id_last4", required=True), + ConstructorPart(name="unit", required=True), + ] + + constructors = [Constructor(parts=constructor_parts)] + + # 3. Define compound beacon. + # A compound beacon allows one to serve multiple beacons or attributes from a single index. + # A compound beacon must receive: + # - name: The name of the beacon. Compound beacon values will be written to `aws_ddb_e_[name]`. + # - split: A character separating parts in a compound beacon + # A compound beacon may also receive: + # - encrypted: A list of encrypted parts. This is effectively a list of beacons. We provide the list + # that we created above. + # - constructors: A list of constructors. This is an ordered list of possible ways to create a beacon. + # We have not defined any constructors here; see the complex example for how to do this. + # The client will provide a default constructor, which will write a compound beacon as: + # all signed parts in the order they are added to the signed list; + # all encrypted parts in order they are added to the encrypted list; all parts required. + # In this example, we expect compound beacons to be written as + # `L-XXX.U-YYYYYYYY`, since our encrypted list looks like + # [last4EncryptedPart, unitEncryptedPart]. + # - signed: A list of signed parts, i.e. plaintext attributes. This would be provided if we + # wanted to use plaintext values as part of constructing our compound beacon. We do not + # provide this here; see the Complex example for an example. + compound_beacon_list = [CompoundBeacon(name="last4UnitCompound", constructors=constructors, split=".")] + + # 4. Configure the Keystore + # These are the same constructions as in the Basic example, which describes these in more detail. + keystore = KeyStore( + config=KeyStoreConfig( + ddb_client=boto3.client("dynamodb"), + ddb_table_name=branch_key_ddb_table_name, + logical_key_store_name=branch_key_ddb_table_name, + kms_client=boto3.client("kms"), + kms_configuration=KMSConfigurationKmsKeyArn(value=branch_key_wrapping_kms_key_arn), + ) + ) + + # 5. Create BeaconVersion. + # This is similar to the Basic example, except we have also provided a compoundBeaconList. + # We must also continue to provide all of the standard beacons that compose a compound beacon list. + beacon_versions = [ + BeaconVersion( + encrypted_parts=encrypted_part_list, + standard_beacons=standard_beacon_list, + compound_beacons=compound_beacon_list, + version=1, # MUST be 1 + key_store=keystore, + key_source=BeaconKeySourceSingle(SingleKeyStore(key_id=branch_key_id, cache_ttl=6000)), + ) + ] + + # 6. Create a Hierarchical Keyring + # This is the same configuration as in the Basic example. + mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig()) + + keyring_input = CreateAwsKmsHierarchicalKeyringInput( + branch_key_id=branch_key_id, key_store=keystore, ttl_seconds=6000 + ) + + kms_keyring = mat_prov.create_aws_kms_hierarchical_keyring(input=keyring_input) + + # 7. Configure which attributes are encrypted and/or signed when writing new items. + attribute_actions = { + "work_id": CryptoAction.SIGN_ONLY, # Our partition attribute must be SIGN_ONLY + "inspection_date": CryptoAction.SIGN_ONLY, # Our sort attribute must be SIGN_ONLY + "inspector_id_last4": CryptoAction.ENCRYPT_AND_SIGN, # Beaconized attributes must be encrypted + "unit": CryptoAction.ENCRYPT_AND_SIGN, # Beaconized attributes must be encrypted + } + + # We do not need to define a crypto action on last4UnitCompound. + # We only need to define crypto actions on attributes that we pass to PutItem. + + # 8. Create the DynamoDb Encryption configuration for the table we will be writing to. + # The beaconVersions are added to the search configuration. + table_config = DynamoDbTableEncryptionConfig( + logical_table_name=ddb_table_name, + partition_key_name="work_id", + sort_key_name="inspection_date", + attribute_actions_on_encrypt=attribute_actions, + keyring=kms_keyring, + search=SearchConfig(write_version=1, versions=beacon_versions), # MUST be 1 + ) + + table_configs = {ddb_table_name: table_config} + tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) + + # 9. Create an item with both attributes used in the compound beacon. + item = { + "work_id": {"S": "9ce39272-8068-4efd-a211-cd162ad65d4c"}, + "inspection_date": {"S": "2023-06-13"}, + "inspector_id_last4": {"S": "5678"}, + "unit": {"S": "011899988199"}, + } + + # 10. If developing or debugging, verify config by checking compound beacon values directly + trans = DynamoDbEncryptionTransforms(config=tables_config) + + resolve_input = ResolveAttributesInput(table_name=ddb_table_name, item=item, version=1) + + resolve_output = trans.resolve_attributes(input=resolve_input) + + # VirtualFields is empty because we have no Virtual Fields configured + assert not resolve_output.virtual_fields + + # Verify that CompoundBeacons has the expected value + cbs = {"last4UnitCompound": "L-5678.U-011899988199"} + assert resolve_output.compound_beacons == cbs + # Note : the compound beacon actually stored in the table is not "L-5678.U-011899988199" + # but rather something like "L-abc.U-123", as both parts are EncryptedParts + # and therefore the text is replaced by the associated beacon + + # 11. Create the EncryptedClient + ddb_client = boto3.client("dynamodb") + encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config) + + put_and_query_item_with_compound_beacon(encrypted_ddb_client, ddb_table_name, item) + + # If instead you were working in a multi-threaded context + # it might look like this + with concurrent.futures.ThreadPoolExecutor(max_workers=MAX_CONCURRENT_QUERY_THREADS) as executor: + futures = [] + for _ in range(2 * MAX_CONCURRENT_QUERY_THREADS): + for _ in range(20): + futures.append( + executor.submit(put_and_query_item_with_compound_beacon, encrypted_ddb_client, ddb_table_name, item) + ) + concurrent.futures.wait(futures, timeout=30) diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/virtual_beacon_searchable_encryption_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/virtual_beacon_searchable_encryption_example.py new file mode 100644 index 000000000..681f660cc --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/virtual_beacon_searchable_encryption_example.py @@ -0,0 +1,415 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Example demonstrating DynamoDB encryption using virtual beacons. + +This example demonstrates how to set up a virtual field from two DDB +attributes, create a standard beacon with that field, put an item with +that beacon, and query against that beacon. + +A virtual field is a field consisting of a transformation of one or more attributes in a DDB item. +Virtual fields are useful in querying against encrypted fields that only have a handful of +possible values. They allow you to take fields with few possible values, concatenate +them to other fields, then query against the combined field. This enables using these types of +fields in queries while making it infeasible to identify which beacon values encode +the few possible distinct plaintexts. This is explained in more detail below. +Virtual fields are not stored in the DDB table. However, they are used to construct +a beacon, the value of which is stored. + +For more information on virtual fields, see + https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/beacons.html#virtual-field + +For our example, we will construct a virtual field +from two DDB attributes `state` and `hasTestResult` as `state`+prefix(`hasTestResult`, 1). +We will then create a beacon out of this virtual field and use it to search. + +This example follows a use case of a database that stores customer test result metadata. +Records are indexed by `customer_id` and store a `state` attribute, representing the +US state or territory where the customer lives, and a `hasTestResult` boolean attribute, +representing whether the customer has a "test result" available. (Maybe this represents +some medical test result, and this table stores "result available" metadata.) We assume +that values in these fields are uniformly distributed across all possible values for +these fields (56 for `state`, 2 for `hasTestResult`), and are uniformly distributed across +customer IDs. + +The motivation behind this example is to demonstrate how and why one would use a virtual beacon. +In this example, our table stores records with an encrypted boolean `hasTestResult` attribute. +We would like to be able to query for customers in a given state with a `true` hasTestResult +attribute. + +To be able to execute this query securely and efficiently, we want the following +properties on our table: + 1. Hide the distribution of `hasTestResult` attribute values (i.e. it should be infeasible + to determine the percentage of `true`s to `false`s across the dataset from beaconized + values) + 2. Query against a combination of whether `hasTestResult` is true/false and the `state` field +We cannot achieve these properties with a standard beacon on a true/false attribute. Following +the guidance to choose a beacon length: + https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/choosing-beacon-length.html +For a boolean value (in our case, whether `hasTestResult` is true or false), the acceptable +bounds for beacon length are either 0 or 1. This corresponds to either not storing a beacon +(length 0), or effectively storing another boolean attribute (length 1). With +length 0, this beacon is useless for searching (violating property 2); with length 1, this +beacon may not hide the attribute (violating property 1). +In addition, choosing a longer beacon length does not help us. +Each attribute value is mapped to a distinct beacon. +Since booleans only have 2 possible attribute values, we will still only have 2 possible +beacon values, though those values may be longer. A longer beacon provides no advantages over +beacon of length 1 in this situation. + +A compound beacon also does not help. +To (over)simplify, a compound beacon is a concatenation of standard beacons, +i.e. beacon(`state`)+beacon(`hasTestResult`). +The `hasTestResult` beacon is still visible, so we would still have the problems above. + +To achieve these properties, we instead construct a virtual field and use that in our beacon, +i.e. beacon(`state`+`hasTestResult`). Assuming these fields are well-distributed across +customer IDs and possible values, this gives us both desired properties; we can query against +both attributes while hiding information from the underlying data. This is demonstrated in more +detail below. + +Running this example requires access to a DDB table with the +following primary key configuration: + - Partition key is named "customer_id" with type (S) + - Sort key is named "create_time" with type (S) +This table must have a Global Secondary Index (GSI) configured named "stateAndHasTestResult-index": + - Partition key is named "aws_dbe_b_stateAndHasTestResult" with type (S) + +In this example for storing customer location data, this schema is utilized for the data: + - "customer_id" stores a unique customer identifier + - "create_time" stores a Unix timestamp + - "state" stores an encrypted 2-letter US state or territory abbreviation + (https://www.faa.gov/air_traffic/publications/atpubs/cnt_html/appendix_a.html) + - "hasTestResult" is not part of the schema, but is an attribute utilized in this example. + It stores a boolean attribute (false/true) indicating whether this customer has a test result + available. + +The example requires the following ordered input command line parameters: + 1. DDB table name for table to put/query data from + 2. Branch key ID for a branch key that was previously created in your key store. See the + CreateKeyStoreKeyExample. + 3. Branch key wrapping KMS key ARN for the KMS key used to create the branch key with ID + provided in arg 2 + 4. Branch key DDB table name for the DDB table representing the branch key store +""" +import time + +import boto3 +from aws_cryptographic_material_providers.keystore.client import KeyStore +from aws_cryptographic_material_providers.keystore.config import KeyStoreConfig +from aws_cryptographic_material_providers.keystore.models import KMSConfigurationKmsKeyArn +from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders +from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig +from aws_cryptographic_material_providers.mpl.models import CreateAwsKmsHierarchicalKeyringInput +from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient +from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_transforms.client import ( + DynamoDbEncryptionTransforms, +) +from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_transforms.models import ( + ResolveAttributesInput, +) +from aws_dbesdk_dynamodb.structures.dynamodb import ( + BeaconKeySourceSingle, + BeaconVersion, + DynamoDbTableEncryptionConfig, + DynamoDbTablesEncryptionConfig, + GetPrefix, + SearchConfig, + SingleKeyStore, + StandardBeacon, + VirtualField, + VirtualPart, + VirtualTransformPrefix, +) +from aws_dbesdk_dynamodb.structures.structured_encryption import CryptoAction + +GSI_NAME = "stateAndHasTestResult-index" + + +def put_item_query_item_with_virtual_beacon( + ddb_table_name: str, branch_key_id: str, branch_key_wrapping_kms_key_arn: str, branch_key_ddb_table_name: str +): + """ + Demonstrate using virtual beacons with DynamoDB encryption. + + :param ddb_table_name: The name of the DynamoDB table + :param branch_key_id: Branch key ID for a branch key previously created in key store + :param branch_key_wrapping_kms_key_arn: ARN of KMS key used to create the branch key + :param branch_key_ddb_table_name: Name of DDB table representing the branch key store + """ + # 1. Construct a length-1 prefix virtual transform. + # `hasTestResult` is a binary attribute, containing either `true` or `false`. + # As an example to demonstrate virtual transforms, we will truncate the value + # of `hasTestResult` in the virtual field to the length-1 prefix of the binary value, i.e.: + # - "true" -> "t" + # - "false -> "f" + # This is not necessary. This is done as a demonstration of virtual transforms. + # Virtual transform operations treat all attributes as strings + # (i.e. the boolean value `true` is interpreted as a string "true"), + # so its length-1 prefix is just "t". + length1_prefix_virtual_transform_list = [VirtualTransformPrefix(GetPrefix(length=1))] + + # 2. Construct the VirtualParts required for the VirtualField + has_test_result_part = VirtualPart( + loc="hasTestResult", + # Here, we apply the length-1 prefix virtual transform + trans=length1_prefix_virtual_transform_list, + ) + + state_part = VirtualPart( + loc="state", + # Note that we do not apply any transform to the `state` attribute, + # and the virtual field will read in the attribute as-is. + ) + + # 3. Construct the VirtualField from the VirtualParts + # Note that the order that virtual parts are added to the virtualPartList + # dictates the order in which they are concatenated to build the virtual field. + # You must add virtual parts in the same order on write as you do on read. + virtual_part_list = [state_part, has_test_result_part] + + state_and_has_test_result_field = VirtualField(name="stateAndHasTestResult", parts=virtual_part_list) + + virtual_field_list = [state_and_has_test_result_field] + + # 4. Configure our beacon. + # The virtual field is assumed to hold a US 2-letter state abbreviation + # (56 possible values = 50 states + 6 territories) concatenated with a binary attribute + # (2 possible values: true/false hasTestResult field), we expect a population size of + # 56 * 2 = 112 possible values. + # We will also assume that these values are reasonably well-distributed across + # customer IDs. In practice, this will not be true. We would expect + # more populous states to appear more frequently in the database. + # A more complex analysis would show that a stricter upper bound + # is necessary to account for this by hiding information from the + # underlying distribution. + # + # This link provides guidance for choosing a beacon length: + # https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/choosing-beacon-length.html + # We follow the guidance in the link above to determine reasonable bounds for beacon length: + # - min: log(sqrt(112))/log(2) ~= 3.4, round down to 3 + # - max: log((112/2))/log(2) ~= 5.8, round up to 6 + # You will somehow need to round results to a nearby integer. + # We choose to round to the nearest integer; you might consider a different rounding approach. + # Rounding up will return fewer expected "false positives" in queries, + # leading to fewer decrypt calls and better performance, + # but it is easier to identify which beacon values encode distinct plaintexts. + # Rounding down will return more expected "false positives" in queries, + # leading to more decrypt calls and worse performance, + # but it is harder to identify which beacon values encode distinct plaintexts. + # We can choose a beacon length between 3 and 6: + # - Closer to 3, we expect more "false positives" to be returned, + # making it harder to identify which beacon values encode distinct plaintexts, + # but leading to more decrypt calls and worse performance + # - Closer to 6, we expect fewer "false positives" returned in queries, + # leading to fewer decrypt calls and better performance, + # but it is easier to identify which beacon values encode distinct plaintexts. + # As an example, we will choose 5. + # Values stored in aws_dbe_b_stateAndHasTestResult will be 5 bits long (0x00 - 0x1f) + # There will be 2^5 = 32 possible HMAC values. + # With a well-distributed dataset (112 values), for a particular beacon we expect + # (112/32) = 3.5 combinations of abbreviation + true/false attribute + # sharing that beacon value. + standard_beacon_list = [ + StandardBeacon( + # This name is the same as our virtual field's name above + name="stateAndHasTestResult", + length=5, + ) + ] + + # 5. Configure Keystore. + # This example expects that you have already set up a KeyStore with a single branch key. + # See the "CreateKeyStoreTableExample" and "CreateKeyStoreKeyExample" files for how to do this. + # After you create a branch key, you should persist its ID for use in this example. + keystore = KeyStore( + config=KeyStoreConfig( + ddb_client=boto3.client("dynamodb"), + ddb_table_name=branch_key_ddb_table_name, + logical_key_store_name=branch_key_ddb_table_name, + kms_client=boto3.client("kms"), + kms_configuration=KMSConfigurationKmsKeyArn(value=branch_key_wrapping_kms_key_arn), + ) + ) + + # 6. Create BeaconVersion. + # The BeaconVersion inside the list holds the list of beacons on the table. + # The BeaconVersion also stores information about the keystore. + # BeaconVersion must be provided: + # - keyStore: The keystore configured in the previous step. + # - keySource: A configuration for the key source. + # For simple use cases, we can configure a 'singleKeySource' which + # statically configures a single beaconKey. That is the approach this example takes. + # For use cases where you want to use different beacon keys depending on the data + # (for example if your table holds data for multiple tenants, and you want to use + # a different beacon key per tenant), look into configuring a MultiKeyStore: + # https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/searchable-encryption-multitenant.html + # We also provide our standard beacon list and virtual fields here. + beacon_versions = [ + BeaconVersion( + virtual_fields=virtual_field_list, + standard_beacons=standard_beacon_list, + version=1, # MUST be 1 + key_store=keystore, + key_source=BeaconKeySourceSingle( + SingleKeyStore( + # `key_id` references a beacon key. + # For every branch key we create in the keystore, + # we also create a beacon key. + # This beacon key is not the same as the branch key, + # but is created with the same ID as the branch key. + key_id=branch_key_id, + cache_ttl=6000, + ) + ), + ) + ] + + # 7. Create a Hierarchical Keyring + # This is a KMS keyring that utilizes the keystore table. + # This config defines how items are encrypted and decrypted. + # NOTE: You should configure this to use the same keystore as your search config. + mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig()) + + keyring_input = CreateAwsKmsHierarchicalKeyringInput( + branch_key_id=branch_key_id, key_store=keystore, ttl_seconds=6000 + ) + + kms_keyring = mat_prov.create_aws_kms_hierarchical_keyring(input=keyring_input) + + # 8. 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 + # Any attributes that will be used in beacons must be configured as ENCRYPT_AND_SIGN. + attribute_actions = { + "customer_id": CryptoAction.SIGN_ONLY, # Our partition attribute must be SIGN_ONLY + "create_time": CryptoAction.SIGN_ONLY, # Our sort attribute must be SIGN_ONLY + "state": CryptoAction.ENCRYPT_AND_SIGN, # Beaconized attributes must be encrypted + "hasTestResult": CryptoAction.ENCRYPT_AND_SIGN, # Beaconized attributes must be encrypted + } + + # 9. Create the DynamoDb Encryption configuration for the table we will be writing to. + # The beaconVersions are added to the search configuration. + table_config = DynamoDbTableEncryptionConfig( + logical_table_name=ddb_table_name, + partition_key_name="customer_id", + sort_key_name="create_time", + attribute_actions_on_encrypt=attribute_actions, + keyring=kms_keyring, + search=SearchConfig(write_version=1, versions=beacon_versions), # MUST be 1 + ) + + table_configs = {ddb_table_name: table_config} + tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) + + # 10. Create test items + + # Create item with hasTestResult=true + item_with_has_test_result = { + "customer_id": {"S": "ABC-123"}, + "create_time": {"N": "1681495205"}, + "state": {"S": "CA"}, + "hasTestResult": {"BOOL": True}, + } + + # Create item with hasTestResult=false + item_with_no_has_test_result = { + "customer_id": {"S": "DEF-456"}, + "create_time": {"N": "1681495205"}, + "state": {"S": "CA"}, + "hasTestResult": {"BOOL": False}, + } + + # 11. If developing or debugging, verify config by checking virtual field values directly + trans = DynamoDbEncryptionTransforms(config=tables_config) + + resolve_input = ResolveAttributesInput(table_name=ddb_table_name, item=item_with_has_test_result, version=1) + + resolve_output = trans.resolve_attributes(input=resolve_input) + + # CompoundBeacons is empty because we have no Compound Beacons configured + assert not resolve_output.compound_beacons + + # Verify that VirtualFields has the expected value + vf = {"stateAndHasTestResult": "CAt"} + assert resolve_output.virtual_fields == vf + + # 12. Create the EncryptedClient + ddb_client = boto3.client("dynamodb") + encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config) + + # 13. Put two items into our table using the above client. + # The two items will differ only in their `customer_id` attribute (primary key) + # and their `hasTestResult` attribute. + # We will query against these items to demonstrate how to use our setup above + # to query against our `stateAndHasTestResult` beacon. + # Before the item gets sent to DynamoDb, it will be encrypted + # client-side, according to our configuration. + # Since our configuration includes a beacon on a virtual field named + # `stateAndHasTestResult`, the client will add an attribute + # to the item with name `aws_dbe_b_stateAndHasTestResult`. + # Its value will be an HMAC truncated to as many bits as the + # beacon's `length` parameter; i.e. 5. + put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item_with_has_test_result) + # Assert PutItem was successful + assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item_with_no_has_test_result) + # Assert PutItem was successful + assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # 14. Query by stateAndHasTestResult attribute. + # Note that we are constructing the query as if we were querying on plaintext values. + # However, the DDB encryption client will detect that this attribute name has a beacon configured. + # The client will add the beaconized attribute name and attribute value to the query, + # and transform the query to use the beaconized name and value. + # Internally, the client will query for and receive all items with a matching HMAC value in the beacon field. + # This may include a number of "false positives" with different ciphertext, but the same truncated HMAC. + # e.g. if truncate(HMAC("CAt"), 5) == truncate(HMAC("DCf"), 5), the query will return both items. + # The client will decrypt all returned items to determine which ones have the expected attribute values, + # and only surface items with the correct plaintext to the user. + # This procedure is internal to the client and is abstracted away from the user; + # e.g. the user will only see "CAt" and never "DCf", though the actual query returned both. + expression_attribute_names = {"#stateAndHasTestResult": "stateAndHasTestResult"} + + # We are querying for the item with `state`="CA" and `hasTestResult`=`true`. + # Since we added virtual parts as `state` then `hasTestResult`, + # we must write our query expression in the same order. + # We constructed our virtual field as `state`+`hasTestResult`, + # so we add the two parts in that order. + # Since we also created a virtual transform that truncated `hasTestResult` + # to its length-1 prefix, i.e. "true" -> "t", + # we write that field as its length-1 prefix in the query. + expression_attribute_values = {":stateAndHasTestResult": {"S": "CAt"}} + + # GSIs do not update instantly + # so if the results come back empty + # we retry after a short sleep + for _ in range(10): + query_response = encrypted_ddb_client.query( + TableName=ddb_table_name, + IndexName=GSI_NAME, + KeyConditionExpression="#stateAndHasTestResult = :stateAndHasTestResult", + ExpressionAttributeNames=expression_attribute_names, + ExpressionAttributeValues=expression_attribute_values, + ) + + # Validate query was returned successfully + assert query_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + items = query_response.get("Items", []) + # if no results, sleep and try again + if not items: + time.sleep(0.02) + continue + + # Validate only 1 item was returned: the item with the expected attributes + assert len(items) == 1 + returned_item = items[0] + # Validate the item has the expected attributes + assert returned_item["state"]["S"] == "CA" + assert returned_item["hasTestResult"]["BOOL"] is True + break diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/test_basic_searchable_encryption_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/test_basic_searchable_encryption_example.py new file mode 100644 index 000000000..0292c26c2 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/test_basic_searchable_encryption_example.py @@ -0,0 +1,40 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test basic searchable encryption example.""" +import time + +import pytest + +from ...src.create_keystore_key_example import keystore_create_key +from ...src.searchable_encryption.basic_searchable_encryption_example import put_item_query_item_with_beacon +from ..cleanup import delete_branch_key +from .searchable_encryption_test_utils import ( + TEST_BRANCH_KEY_WRAPPING_KMS_KEY_ARN, + TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME, + TEST_LOGICAL_KEYSTORE_NAME, + UNIT_INSPECTION_TEST_DDB_TABLE_NAME, +) + +pytestmark = [pytest.mark.examples] + + +def test_basic_example(): + """Test basic searchable encryption example.""" + # Create new branch key for test + key_id = keystore_create_key( + TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME, TEST_LOGICAL_KEYSTORE_NAME, TEST_BRANCH_KEY_WRAPPING_KMS_KEY_ARN + ) + + # Key creation is eventually consistent, so wait 5 seconds to decrease the likelihood + # our test fails due to eventual consistency issues. + time.sleep(5) + + put_item_query_item_with_beacon( + UNIT_INSPECTION_TEST_DDB_TABLE_NAME, + key_id, + TEST_BRANCH_KEY_WRAPPING_KMS_KEY_ARN, + TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME, + ) + + # Cleanup Branch Key + delete_branch_key(key_id, TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME, None) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/test_beacon_styles_searchable_encryption_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/test_beacon_styles_searchable_encryption_example.py new file mode 100644 index 000000000..65e7f929b --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/test_beacon_styles_searchable_encryption_example.py @@ -0,0 +1,42 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test beacon styles searchable encryption example.""" +import time + +import pytest + +from ...src.create_keystore_key_example import keystore_create_key +from ...src.searchable_encryption.beacon_styles_searchable_encryption_example import ( + put_item_query_item_with_beacon_styles, +) +from ..cleanup import delete_branch_key +from .searchable_encryption_test_utils import ( + TEST_BRANCH_KEY_WRAPPING_KMS_KEY_ARN, + TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME, + TEST_LOGICAL_KEYSTORE_NAME, + UNIT_INSPECTION_TEST_DDB_TABLE_NAME, +) + +pytestmark = [pytest.mark.examples] + + +def test_beacon_styles_item_encrypt_decrypt(): + """Test beacon styles searchable encryption example.""" + # Create new branch key for test + key_id = keystore_create_key( + TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME, TEST_LOGICAL_KEYSTORE_NAME, TEST_BRANCH_KEY_WRAPPING_KMS_KEY_ARN + ) + + # Key creation is eventually consistent, so wait 5 seconds to decrease the likelihood + # our test fails due to eventual consistency issues. + time.sleep(5) + + put_item_query_item_with_beacon_styles( + UNIT_INSPECTION_TEST_DDB_TABLE_NAME, + key_id, + TEST_BRANCH_KEY_WRAPPING_KMS_KEY_ARN, + TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME, + ) + + # Cleanup Branch Key + delete_branch_key(key_id, TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME, None) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/test_compound_beacon_searchable_encryption_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/test_compound_beacon_searchable_encryption_example.py new file mode 100644 index 000000000..6d05fbf75 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/test_compound_beacon_searchable_encryption_example.py @@ -0,0 +1,42 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test compound beacon searchable encryption example.""" +import time + +import pytest + +from ...src.create_keystore_key_example import keystore_create_key +from ...src.searchable_encryption.compound_beacon_searchable_encryption_example import ( + put_item_query_item_with_compound_beacon, +) +from ..cleanup import delete_branch_key +from .searchable_encryption_test_utils import ( + TEST_BRANCH_KEY_WRAPPING_KMS_KEY_ARN, + TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME, + TEST_LOGICAL_KEYSTORE_NAME, + UNIT_INSPECTION_TEST_DDB_TABLE_NAME, +) + +pytestmark = [pytest.mark.examples] + + +def test_compound_item_encrypt_decrypt(): + """Test compound beacon searchable encryption example.""" + # Create new branch key for test + key_id = keystore_create_key( + TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME, TEST_LOGICAL_KEYSTORE_NAME, TEST_BRANCH_KEY_WRAPPING_KMS_KEY_ARN + ) + + # Key creation is eventually consistent, so wait 5 seconds to decrease the likelihood + # our test fails due to eventual consistency issues. + time.sleep(5) + + put_item_query_item_with_compound_beacon( + UNIT_INSPECTION_TEST_DDB_TABLE_NAME, + key_id, + TEST_BRANCH_KEY_WRAPPING_KMS_KEY_ARN, + TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME, + ) + + # Cleanup Branch Key + delete_branch_key(key_id, TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME, None) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/test_virtual_beacon_searchable_encryption_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/test_virtual_beacon_searchable_encryption_example.py new file mode 100644 index 000000000..2ecda0a48 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/test_virtual_beacon_searchable_encryption_example.py @@ -0,0 +1,43 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test virtual beacon searchable encryption example.""" +import time + +import pytest + +from DynamoDBEncryption.src.create_keystore_key_example import keystore_create_key +from DynamoDBEncryption.src.searchable_encryption.virtual_beacon_searchable_encryption_example import ( + put_item_query_item_with_virtual_beacon, +) + +from ..cleanup import delete_branch_key +from .searchable_encryption_test_utils import ( + SIMPLE_BEACON_TEST_DDB_TABLE_NAME, + TEST_BRANCH_KEY_WRAPPING_KMS_KEY_ARN, + TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME, + TEST_LOGICAL_KEYSTORE_NAME, +) + +pytestmark = [pytest.mark.examples] + + +def test_virtual_beacon_example(): + """Test virtual beacon searchable encryption example.""" + # Create new branch key for test + key_id = keystore_create_key( + TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME, TEST_LOGICAL_KEYSTORE_NAME, TEST_BRANCH_KEY_WRAPPING_KMS_KEY_ARN + ) + + # Key creation is eventually consistent, so wait 5 seconds to decrease the likelihood + # our test fails due to eventual consistency issues. + time.sleep(5) + + put_item_query_item_with_virtual_beacon( + SIMPLE_BEACON_TEST_DDB_TABLE_NAME, + key_id, + TEST_BRANCH_KEY_WRAPPING_KMS_KEY_ARN, + TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME, + ) + + # Cleanup Branch Key + delete_branch_key(key_id, TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME, None) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/test_get_encrypted_data_key_description_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/test_get_encrypted_data_key_description_example.py new file mode 100644 index 000000000..2ed488ebe --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/test_get_encrypted_data_key_description_example.py @@ -0,0 +1,27 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test get encrypted data key description example.""" +import pytest + +from ..src.get_encrypted_data_key_description_example import get_encrypted_data_key_description +from .test_utils import ( + TEST_DDB_TABLE_NAME, + TEST_KMS_KEY_ID, +) + +pytestmark = [pytest.mark.examples] + + +def test_get_encrypted_data_key_description(): + """Test get encrypted data key description example.""" + get_encrypted_data_key_description( + TEST_DDB_TABLE_NAME, + "partition_key", + "BasicPutGetExample", + "sort_key", + "0", + "aws-kms", + TEST_KMS_KEY_ID, + None, + None, + ) From c19af19285404cbffdf42a3dbbfc89cd336c5a6b Mon Sep 17 00:00:00 2001 From: Darwin Chowdary <39110935+imabhichow@users.noreply.github.com> Date: Tue, 3 Jun 2025 15:00:16 -0700 Subject: [PATCH 2/9] support EncryptedTable --- .../hierarchical_keyring_example/__init__.py | 3 + .../encryption_config.py} | 91 +--- .../example_branch_key_id_supplier.py | 0 .../with_encrypted_client.py | 95 ++++ .../with_encrypted_table.py | 98 ++++ .../kms_ecdh_keyring_example/__init__.py | 3 + .../kms_ecdh_keyring_example/utility.py | 88 ++++ .../with_encrypted_client.py} | 88 +--- .../with_encrypted_table.py | 389 ++++++++++++++ .../src/keyring/kms_rsa_keyring_example.py | 230 --------- .../kms_rsa_keyring_example/__init__.py | 3 + .../encryption_config.py | 114 +++++ .../kms_rsa_keyring_example/utility.py | 62 +++ .../with_encrypted_client.py | 92 ++++ .../with_encrypted_table.py | 95 ++++ .../__init__.py | 3 + .../with_encrypted_client.py} | 6 +- .../with_encrypted_table.py | 190 +++++++ .../mrk_multi_keyring_example/__init__.py | 3 + .../with_encrypted_client.py} | 4 +- .../with_encrypted_table.py | 250 +++++++++ .../keyring/multi_keyring_example/__init__.py | 3 + .../with_encrypted_client.py} | 4 +- .../with_encrypted_table.py | 209 ++++++++ .../raw_aes_keyring_example/__init__.py | 3 + .../encryption_config.py} | 60 +-- .../with_encrypted_client.py | 73 +++ .../with_encrypted_table.py | 76 +++ .../raw_ecdh_keyring_example/__init__.py | 3 + .../raw_ecdh_keyring_example/utility.py | 140 +++++ .../with_encrypted_client.py} | 118 +---- .../with_encrypted_table.py | 483 ++++++++++++++++++ .../src/keyring/raw_rsa_keyring_example.py | 245 --------- .../raw_rsa_keyring_example/__init__.py | 3 + .../encryption_config.py | 103 ++++ .../raw_rsa_keyring_example/utility.py | 76 +++ .../with_encrypted_client.py | 108 ++++ .../with_encrypted_table.py | 111 ++++ .../__init__.py | 3 + .../with_encrypted_client.py} | 0 .../with_encrypted_table.py | 351 +++++++++++++ .../src/scan_error_example/__init__.py | 3 + .../encryption_config.py} | 42 +- .../with_encrypted_client.py | 49 ++ .../with_encrypted_table.py | 51 ++ .../__init__.py | 3 + .../beacon_config.py} | 122 +---- .../with_encrypted_client.py | 130 +++++ .../with_encrypted_table.py | 129 +++++ ...on_styles_searchable_encryption_example.py | 298 ----------- .../__init__.py | 3 + .../beacon_config.py | 146 ++++++ .../with_encrypted_client.py | 175 +++++++ .../with_encrypted_table.py | 174 +++++++ .../__init__.py | 3 + .../beacon_config.py} | 167 +----- .../with_encrypted_client.py | 164 ++++++ .../with_encrypted_table.py | 162 ++++++ .../__init__.py | 3 + .../beacon_config.py | 200 ++++++++ .../with_encrypted_client.py} | 226 ++------ .../with_encrypted_table.py | 240 +++++++++ .../hierarchical_keyring_example/__init__.py | 3 + .../test_with_encrypted_client.py | 46 ++ .../test_with_encrypted_table.py | 46 ++ .../kms_ecdh_keyring_example/__init__.py | 3 + .../test_with_encrypted_client.py | 50 ++ .../test_with_encrypted_table.py | 50 ++ .../kms_rsa_keyring_example/__init__.py | 3 + .../test_with_encrypted_client.py | 27 + .../test_with_encrypted_table.py} | 14 +- .../__init__.py | 3 + .../test_with_encrypted_client.py | 24 + .../test_with_encrypted_table.py | 24 + .../mrk_multi_keyring_example/__init__.py | 3 + .../test_with_encrypted_client.py | 23 + .../test_with_encrypted_table.py} | 14 +- .../keyring/multi_keyring_example/__init__.py | 3 + .../test_with_encrypted_client.py | 19 + .../test_with_encrypted_table.py | 19 + .../raw_aes_keyring_example/__init__.py | 3 + .../test_with_encrypted_client.py | 19 + .../test_with_encrypted_table.py | 19 + .../raw_ecdh_keyring_example/__init__.py | 3 + .../test_with_encrypted_client.py} | 42 +- .../test_with_encrypted_table.py | 76 +++ .../raw_rsa_keyring_example/__init__.py | 3 + .../test_with_encrypted_client.py | 29 ++ .../test_with_encrypted_table.py | 29 ++ .../__init__.py | 3 + .../test_with_encrypted_client.py | 43 ++ .../test_with_encrypted_table.py} | 8 +- .../test_hierarchical_keyring_example.py | 37 -- .../keyring/test_kms_ecdh_keyring_example.py | 40 -- ...est_mrk_discovery_multi_keyring_example.py | 22 - .../keyring/test_multi_keyring_example.py | 19 - .../keyring/test_raw_aes_keyring_example.py | 19 - .../keyring/test_raw_rsa_keyring_example.py | 25 - .../test/scan_error_example/__init__.py | 3 + .../test_with_encrypted_client.py | 13 + .../test_with_encrypted_table.py} | 7 +- .../__init__.py | 3 + .../test_with_with_encrypted_client.py | 42 ++ .../test_with_with_encrypted_table.py} | 14 +- .../__init__.py | 3 + .../test_with_with_encrypted_client.py} | 14 +- .../test_with_with_encrypted_table.py | 42 ++ .../__init__.py | 3 + .../test_with_with_encrypted_client.py | 42 ++ .../test_with_with_encrypted_table.py} | 14 +- .../__init__.py | 3 + .../test_with_with_encrypted_client.py} | 15 +- .../test_with_with_encrypted_table.py | 42 ++ 113 files changed, 5810 insertions(+), 1832 deletions(-) create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/keyring/hierarchical_keyring_example/__init__.py rename Examples/runtimes/python/DynamoDBEncryption/src/keyring/{hierarchical_keyring_example.py => hierarchical_keyring_example/encryption_config.py} (64%) rename Examples/runtimes/python/DynamoDBEncryption/src/keyring/{ => hierarchical_keyring_example}/example_branch_key_id_supplier.py (100%) create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/keyring/hierarchical_keyring_example/with_encrypted_client.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/keyring/hierarchical_keyring_example/with_encrypted_table.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_ecdh_keyring_example/__init__.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_ecdh_keyring_example/utility.py rename Examples/runtimes/python/DynamoDBEncryption/src/keyring/{kms_ecdh_keyring_example.py => kms_ecdh_keyring_example/with_encrypted_client.py} (85%) create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_ecdh_keyring_example/with_encrypted_table.py delete mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_rsa_keyring_example.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_rsa_keyring_example/__init__.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_rsa_keyring_example/encryption_config.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_rsa_keyring_example/utility.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_rsa_keyring_example/with_encrypted_client.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_rsa_keyring_example/with_encrypted_table.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/keyring/mrk_discovery_multi_keyring_example/__init__.py rename Examples/runtimes/python/DynamoDBEncryption/src/keyring/{mrk_discovery_multi_keyring_example.py => mrk_discovery_multi_keyring_example/with_encrypted_client.py} (98%) create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/keyring/mrk_discovery_multi_keyring_example/with_encrypted_table.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/keyring/mrk_multi_keyring_example/__init__.py rename Examples/runtimes/python/DynamoDBEncryption/src/keyring/{mrk_multi_keyring_example.py => mrk_multi_keyring_example/with_encrypted_client.py} (98%) create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/keyring/mrk_multi_keyring_example/with_encrypted_table.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/keyring/multi_keyring_example/__init__.py rename Examples/runtimes/python/DynamoDBEncryption/src/keyring/{multi_keyring_example.py => multi_keyring_example/with_encrypted_client.py} (98%) create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/keyring/multi_keyring_example/with_encrypted_table.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_aes_keyring_example/__init__.py rename Examples/runtimes/python/DynamoDBEncryption/src/keyring/{raw_aes_keyring_example.py => raw_aes_keyring_example/encryption_config.py} (64%) create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_aes_keyring_example/with_encrypted_client.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_aes_keyring_example/with_encrypted_table.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_ecdh_keyring_example/__init__.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_ecdh_keyring_example/utility.py rename Examples/runtimes/python/DynamoDBEncryption/src/keyring/{raw_ecdh_keyring_example.py => raw_ecdh_keyring_example/with_encrypted_client.py} (84%) create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_ecdh_keyring_example/with_encrypted_table.py delete mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_rsa_keyring_example.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_rsa_keyring_example/__init__.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_rsa_keyring_example/encryption_config.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_rsa_keyring_example/utility.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_rsa_keyring_example/with_encrypted_client.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_rsa_keyring_example/with_encrypted_table.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/keyring/shared_cache_across_hierarchical_keyrings_example/__init__.py rename Examples/runtimes/python/DynamoDBEncryption/src/keyring/{shared_cache_across_hierarchical_keyrings_example.py => shared_cache_across_hierarchical_keyrings_example/with_encrypted_client.py} (100%) create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/keyring/shared_cache_across_hierarchical_keyrings_example/with_encrypted_table.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/scan_error_example/__init__.py rename Examples/runtimes/python/DynamoDBEncryption/src/{scan_error_example.py => scan_error_example/encryption_config.py} (81%) create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/scan_error_example/with_encrypted_client.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/scan_error_example/with_encrypted_table.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/basic_searchable_encryption_example/__init__.py rename Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/{basic_searchable_encryption_example.py => basic_searchable_encryption_example/beacon_config.py} (65%) create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/basic_searchable_encryption_example/with_encrypted_client.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/basic_searchable_encryption_example/with_encrypted_table.py delete mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/beacon_styles_searchable_encryption_example.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/beacon_styles_searchable_encryption_example/__init__.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/beacon_styles_searchable_encryption_example/beacon_config.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/beacon_styles_searchable_encryption_example/with_encrypted_client.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/beacon_styles_searchable_encryption_example/with_encrypted_table.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/compound_beacon_searchable_encryption_example/__init__.py rename Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/{compound_beacon_searchable_encryption_example.py => compound_beacon_searchable_encryption_example/beacon_config.py} (50%) create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/compound_beacon_searchable_encryption_example/with_encrypted_client.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/compound_beacon_searchable_encryption_example/with_encrypted_table.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/virtual_beacon_searchable_encryption_example/__init__.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/virtual_beacon_searchable_encryption_example/beacon_config.py rename Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/{virtual_beacon_searchable_encryption_example.py => virtual_beacon_searchable_encryption_example/with_encrypted_client.py} (52%) create mode 100644 Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/virtual_beacon_searchable_encryption_example/with_encrypted_table.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/test/keyring/hierarchical_keyring_example/__init__.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/test/keyring/hierarchical_keyring_example/test_with_encrypted_client.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/test/keyring/hierarchical_keyring_example/test_with_encrypted_table.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/test/keyring/kms_ecdh_keyring_example/__init__.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/test/keyring/kms_ecdh_keyring_example/test_with_encrypted_client.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/test/keyring/kms_ecdh_keyring_example/test_with_encrypted_table.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/test/keyring/kms_rsa_keyring_example/__init__.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/test/keyring/kms_rsa_keyring_example/test_with_encrypted_client.py rename Examples/runtimes/python/DynamoDBEncryption/test/keyring/{test_kms_rsa_keyring_example.py => kms_rsa_keyring_example/test_with_encrypted_table.py} (60%) create mode 100644 Examples/runtimes/python/DynamoDBEncryption/test/keyring/mrk_discovery_multi_keyring_example/__init__.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/test/keyring/mrk_discovery_multi_keyring_example/test_with_encrypted_client.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/test/keyring/mrk_discovery_multi_keyring_example/test_with_encrypted_table.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/test/keyring/mrk_multi_keyring_example/__init__.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/test/keyring/mrk_multi_keyring_example/test_with_encrypted_client.py rename Examples/runtimes/python/DynamoDBEncryption/test/keyring/{test_mrk_multi_keyring_example.py => mrk_multi_keyring_example/test_with_encrypted_table.py} (52%) create mode 100644 Examples/runtimes/python/DynamoDBEncryption/test/keyring/multi_keyring_example/__init__.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/test/keyring/multi_keyring_example/test_with_encrypted_client.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/test/keyring/multi_keyring_example/test_with_encrypted_table.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/test/keyring/raw_aes_keyring_example/__init__.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/test/keyring/raw_aes_keyring_example/test_with_encrypted_client.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/test/keyring/raw_aes_keyring_example/test_with_encrypted_table.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/test/keyring/raw_ecdh_keyring_example/__init__.py rename Examples/runtimes/python/DynamoDBEncryption/test/keyring/{test_raw_ecdh_keyring_example.py => raw_ecdh_keyring_example/test_with_encrypted_client.py} (60%) create mode 100644 Examples/runtimes/python/DynamoDBEncryption/test/keyring/raw_ecdh_keyring_example/test_with_encrypted_table.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/test/keyring/raw_rsa_keyring_example/__init__.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/test/keyring/raw_rsa_keyring_example/test_with_encrypted_client.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/test/keyring/raw_rsa_keyring_example/test_with_encrypted_table.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/test/keyring/shared_cache_across_hierarchical_keyrings_example/__init__.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/test/keyring/shared_cache_across_hierarchical_keyrings_example/test_with_encrypted_client.py rename Examples/runtimes/python/DynamoDBEncryption/test/keyring/{test_shared_cache_across_hierarchical_keyrings_example.py => shared_cache_across_hierarchical_keyrings_example/test_with_encrypted_table.py} (83%) delete mode 100644 Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_hierarchical_keyring_example.py delete mode 100644 Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_kms_ecdh_keyring_example.py delete mode 100644 Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_mrk_discovery_multi_keyring_example.py delete mode 100644 Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_multi_keyring_example.py delete mode 100644 Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_raw_aes_keyring_example.py delete mode 100644 Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_raw_rsa_keyring_example.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/test/scan_error_example/__init__.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/test/scan_error_example/test_with_encrypted_client.py rename Examples/runtimes/python/DynamoDBEncryption/test/{test_scan_error_example.py => scan_error_example/test_with_encrypted_table.py} (53%) create mode 100644 Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/basic_searchable_encryption_example/__init__.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/basic_searchable_encryption_example/test_with_with_encrypted_client.py rename Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/{test_basic_searchable_encryption_example.py => basic_searchable_encryption_example/test_with_with_encrypted_table.py} (71%) create mode 100644 Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/beacon_styles_searchable_encryption_example/__init__.py rename Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/{test_beacon_styles_searchable_encryption_example.py => beacon_styles_searchable_encryption_example/test_with_with_encrypted_client.py} (73%) create mode 100644 Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/beacon_styles_searchable_encryption_example/test_with_with_encrypted_table.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/compound_beacon_searchable_encryption_example/__init__.py create mode 100644 Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/compound_beacon_searchable_encryption_example/test_with_with_encrypted_client.py rename Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/{test_compound_beacon_searchable_encryption_example.py => compound_beacon_searchable_encryption_example/test_with_with_encrypted_table.py} (73%) create mode 100644 Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/virtual_beacon_searchable_encryption_example/__init__.py rename Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/{test_virtual_beacon_searchable_encryption_example.py => virtual_beacon_searchable_encryption_example/test_with_with_encrypted_client.py} (72%) create mode 100644 Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/virtual_beacon_searchable_encryption_example/test_with_with_encrypted_table.py diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/hierarchical_keyring_example/__init__.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/hierarchical_keyring_example/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/hierarchical_keyring_example/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/hierarchical_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/hierarchical_keyring_example/encryption_config.py similarity index 64% rename from Examples/runtimes/python/DynamoDBEncryption/src/keyring/hierarchical_keyring_example.py rename to Examples/runtimes/python/DynamoDBEncryption/src/keyring/hierarchical_keyring_example/encryption_config.py index 9c68125e6..02614d105 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/hierarchical_keyring_example.py +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/hierarchical_keyring_example/encryption_config.py @@ -1,49 +1,10 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 """ -Example demonstrating DynamoDb Encryption using a Hierarchical Keyring. +Configuration module for hierarchical keyring encryption setup. -This example sets up DynamoDb Encryption for the AWS SDK client -using the Hierarchical Keyring, which establishes a key hierarchy -where "branch" keys are persisted in DynamoDb. -These branch keys are used to protect your data keys, -and these branch keys are themselves protected by a root KMS Key. - -Establishing a key hierarchy like this has two benefits: - -First, by caching the branch key material, and only calling back -to KMS to re-establish authentication regularly according to your configured TTL, -you limit how often you need to call back to KMS to protect your data. -This is a performance/security tradeoff, where your authentication, audit, and -logging from KMS is no longer one-to-one with every encrypt or decrypt call. -However, the benefit is that you no longer have to make a -network call to KMS for every encrypt or decrypt. - -Second, this key hierarchy makes it easy to hold multi-tenant data -that is isolated per branch key in a single DynamoDb table. -You can create a branch key for each tenant in your table, -and encrypt all that tenant's data under that distinct branch key. -On decrypt, you can either statically configure a single branch key -to ensure you are restricting decryption to a single tenant, -or you can implement an interface that lets you map the primary key on your items -to the branch key that should be responsible for decrypting that data. - -This example then demonstrates configuring a Hierarchical Keyring -with a Branch Key ID Supplier to encrypt and decrypt data for -two separate tenants. - -Running this example requires access to the DDB Table whose name -is provided in CLI arguments. -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 (S) - -This example also requires using a KMS Key whose ARN -is provided in CLI arguments. You need the following access -on this key: - - GenerateDataKeyWithoutPlaintext - - Decrypt +This module provides the common encryption configuration used by both +EncryptedClient and EncryptedTable examples. """ import boto3 @@ -57,7 +18,6 @@ CreateAwsKmsHierarchicalKeyringInput, DefaultCache, ) -from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb.client import DynamoDbEncryption from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb.config import ( DynamoDbEncryptionConfig, @@ -74,16 +34,16 @@ from .example_branch_key_id_supplier import ExampleBranchKeyIdSupplier -def hierarchical_keyring_get_item_put_item( +def create_encryption_config( ddb_table_name: str, tenant1_branch_key_id: str, tenant2_branch_key_id: str, keystore_table_name: str, logical_keystore_name: str, kms_key_id: str, -): +) -> DynamoDbTablesEncryptionConfig: """ - Demonstrate using a hierarchical keyring with multiple tenants. + Create the encryption configuration for DynamoDB encryption. :param ddb_table_name: The name of the DynamoDB table :param tenant1_branch_key_id: Branch key ID for tenant 1 @@ -91,6 +51,7 @@ def hierarchical_keyring_get_item_put_item( :param keystore_table_name: The name of the KeyStore DynamoDB table :param logical_keystore_name: The logical name for this keystore :param kms_key_id: The ARN of the KMS key to use + :return: The DynamoDB tables encryption configuration """ # Initial KeyStore Setup: This example requires that you have already # created your KeyStore, and have populated it with two new branch keys. @@ -190,40 +151,4 @@ def hierarchical_keyring_get_item_put_item( ) table_configs = {ddb_table_name: table_config} - tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) - - # 7. Create the EncryptedClient - ddb_client = boto3.client("dynamodb") - encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config) - - # 8. Put an item into our table using the above client. - # Before the item gets sent to DynamoDb, it will be encrypted - # client-side, according to our configuration. - # Because the item we are writing uses "tenantId1" as our partition value, - # based on the code we wrote in the ExampleBranchKeySupplier, - # `tenant1_branch_key_id` will be used to encrypt this item. - item = { - "partition_key": {"S": "tenant1Id"}, - "sort_key": {"N": "0"}, - "tenant_sensitive_data": {"S": "encrypt and sign me!"}, - } - - put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item) - - # Demonstrate that PutItem succeeded - assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 - - # 9. Get the item back from our table using the same client. - # The client will decrypt the item client-side, and return - # back the original item. - # Because the returned item's partition value is "tenantId1", - # based on the code we wrote in the ExampleBranchKeySupplier, - # `tenant1_branch_key_id` will be used to decrypt this item. - key_to_get = {"partition_key": {"S": "tenant1Id"}, "sort_key": {"N": "0"}} - - get_response = encrypted_ddb_client.get_item(TableName=ddb_table_name, Key=key_to_get) - - # Demonstrate that GetItem succeeded and returned the decrypted item - assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200 - returned_item = get_response["Item"] - assert returned_item["tenant_sensitive_data"]["S"] == "encrypt and sign me!" + return DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/example_branch_key_id_supplier.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/hierarchical_keyring_example/example_branch_key_id_supplier.py similarity index 100% rename from Examples/runtimes/python/DynamoDBEncryption/src/keyring/example_branch_key_id_supplier.py rename to Examples/runtimes/python/DynamoDBEncryption/src/keyring/hierarchical_keyring_example/example_branch_key_id_supplier.py diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/hierarchical_keyring_example/with_encrypted_client.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/hierarchical_keyring_example/with_encrypted_client.py new file mode 100644 index 000000000..76aa4d92d --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/hierarchical_keyring_example/with_encrypted_client.py @@ -0,0 +1,95 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Example demonstrating DynamoDb Encryption using a Hierarchical Keyring with EncryptedClient. + +This example sets up DynamoDb Encryption for the AWS SDK client +using the Hierarchical Keyring, which establishes a key hierarchy +where "branch" keys are persisted in DynamoDb. +These branch keys are used to protect your data keys, +and these branch keys are themselves protected by a root KMS Key. + +Running this example requires access to the DDB Table whose name +is provided in CLI arguments. +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 (S) + +This example also requires using a KMS Key whose ARN +is provided in CLI arguments. You need the following access +on this key: + - GenerateDataKeyWithoutPlaintext + - Decrypt +""" + +import boto3 +from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient + +from .encryption_config import create_encryption_config + + +def hierarchical_keyring_client_example( + ddb_table_name: str, + tenant1_branch_key_id: str, + tenant2_branch_key_id: str, + keystore_table_name: str, + logical_keystore_name: str, + kms_key_id: str, +): + """ + Demonstrate using a hierarchical keyring with multiple tenants using EncryptedClient. + + :param ddb_table_name: The name of the DynamoDB table + :param tenant1_branch_key_id: Branch key ID for tenant 1 + :param tenant2_branch_key_id: Branch key ID for tenant 2 + :param keystore_table_name: The name of the KeyStore DynamoDB table + :param logical_keystore_name: The logical name for this keystore + :param kms_key_id: The ARN of the KMS key to use + """ + # 1. Create the DynamoDb Encryption configuration for the table we will be writing to. + # See beacon_config.py in this directory for detailed steps on the encryption configuration. + tables_config = create_encryption_config( + ddb_table_name=ddb_table_name, + tenant1_branch_key_id=tenant1_branch_key_id, + tenant2_branch_key_id=tenant2_branch_key_id, + keystore_table_name=keystore_table_name, + logical_keystore_name=logical_keystore_name, + kms_key_id=kms_key_id, + ) + + # 2. Create the EncryptedClient + ddb_client = boto3.client("dynamodb") + encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config) + + # 3. Put an item into our table using the above client. + # Before the item gets sent to DynamoDb, it will be encrypted + # client-side, according to our configuration. + # Because the item we are writing uses "tenantId1" as our partition value, + # based on the code we wrote in the ExampleBranchKeySupplier, + # `tenant1_branch_key_id` will be used to encrypt this item. + item = { + "partition_key": {"S": "tenant1Id"}, + "sort_key": {"N": "0"}, + "tenant_sensitive_data": {"S": "encrypt and sign me!"}, + } + + put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item) + + # Demonstrate that PutItem succeeded + assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # 4. Get the item back from our table using the same client. + # The client will decrypt the item client-side, and return + # back the original item. + # Because the returned item's partition value is "tenantId1", + # based on the code we wrote in the ExampleBranchKeySupplier, + # `tenant1_branch_key_id` will be used to decrypt this item. + key_to_get = {"partition_key": {"S": "tenant1Id"}, "sort_key": {"N": "0"}} + + get_response = encrypted_ddb_client.get_item(TableName=ddb_table_name, Key=key_to_get) + + # Demonstrate that GetItem succeeded and returned the decrypted item + assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + returned_item = get_response["Item"] + assert returned_item["tenant_sensitive_data"]["S"] == "encrypt and sign me!" diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/hierarchical_keyring_example/with_encrypted_table.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/hierarchical_keyring_example/with_encrypted_table.py new file mode 100644 index 000000000..b42787a85 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/hierarchical_keyring_example/with_encrypted_table.py @@ -0,0 +1,98 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Example demonstrating DynamoDb Encryption using a Hierarchical Keyring with EncryptedTable. + +This example sets up DynamoDb Encryption for the AWS SDK table +using the Hierarchical Keyring, which establishes a key hierarchy +where "branch" keys are persisted in DynamoDb. +These branch keys are used to protect your data keys, +and these branch keys are themselves protected by a root KMS Key. + +Running this example requires access to the DDB Table whose name +is provided in CLI arguments. +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 (S) + +This example also requires using a KMS Key whose ARN +is provided in CLI arguments. You need the following access +on this key: + - GenerateDataKeyWithoutPlaintext + - Decrypt +""" + +import boto3 +from aws_dbesdk_dynamodb.encrypted.table import EncryptedTable + +from .encryption_config import create_encryption_config + + +def hierarchical_keyring_table_example( + ddb_table_name: str, + tenant1_branch_key_id: str, + tenant2_branch_key_id: str, + keystore_table_name: str, + logical_keystore_name: str, + kms_key_id: str, +): + """ + Demonstrate using a hierarchical keyring with multiple tenants using EncryptedTable. + + :param ddb_table_name: The name of the DynamoDB table + :param tenant1_branch_key_id: Branch key ID for tenant 1 + :param tenant2_branch_key_id: Branch key ID for tenant 2 + :param keystore_table_name: The name of the KeyStore DynamoDB table + :param logical_keystore_name: The logical name for this keystore + :param kms_key_id: The ARN of the KMS key to use + """ + # 1. Create the DynamoDb Encryption configuration for the table we will be writing to. + # See encryption_config.py in this directory for detailed steps on the encryption configuration. + tables_config = create_encryption_config( + ddb_table_name=ddb_table_name, + tenant1_branch_key_id=tenant1_branch_key_id, + tenant2_branch_key_id=tenant2_branch_key_id, + keystore_table_name=keystore_table_name, + logical_keystore_name=logical_keystore_name, + kms_key_id=kms_key_id, + ) + + # 2. Create the EncryptedTable + ddb_table = boto3.resource("dynamodb").Table(ddb_table_name) + encrypted_table = EncryptedTable( + table=ddb_table, + encryption_config=tables_config, + ) + + # 3. Put an item into our table using the above client. + # Before the item gets sent to DynamoDb, it will be encrypted + # client-side, according to our configuration. + # Because the item we are writing uses "tenantId1" as our partition value, + # based on the code we wrote in the ExampleBranchKeySupplier, + # `tenant1_branch_key_id` will be used to encrypt this item. + item = { + "partition_key": "tenant1Id", + "sort_key": 0, + "tenant_sensitive_data": "encrypt and sign me!", + } + + put_response = encrypted_table.put_item(Item=item) + + # Demonstrate that PutItem succeeded + assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # 4. Get the item back from our table using the same client. + # The client will decrypt the item client-side, and return + # back the original item. + # Because the returned item's partition value is "tenantId1", + # based on the code we wrote in the ExampleBranchKeySupplier, + # `tenant1_branch_key_id` will be used to decrypt this item. + key_to_get = {"partition_key": "tenant1Id", "sort_key": 0} + + get_response = encrypted_table.get_item(Key=key_to_get) + + # Demonstrate that GetItem succeeded and returned the decrypted item + assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + returned_item = get_response["Item"] + assert returned_item["tenant_sensitive_data"] == "encrypt and sign me!" diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_ecdh_keyring_example/__init__.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_ecdh_keyring_example/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_ecdh_keyring_example/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_ecdh_keyring_example/utility.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_ecdh_keyring_example/utility.py new file mode 100644 index 000000000..85947b4ac --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_ecdh_keyring_example/utility.py @@ -0,0 +1,88 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Utility functions for the KMS ECDH keyring example. + +This module provides functions for handling ECC public keys, including: +- Loading public key bytes from a PEM file +- Checking if new public keys should be generated +- Writing public key PEM files for ECC keys +""" + +import pathlib + +import boto3 +from cryptography.hazmat.primitives import serialization + +EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME = "KmsEccKeyringKeyringExamplePublicKeySender.pem" +EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME = "KmsEccKeyringKeyringExamplePublicKeyRecipient.pem" + + +def load_public_key_bytes(ecc_public_key_filename: str) -> bytes: + """ + Load public key bytes from a PEM file. + + :param ecc_public_key_filename: The filename containing the public key + :return: The public key bytes + """ + try: + with open(ecc_public_key_filename, "rb") as f: + public_key_file_bytes = f.read() + public_key = serialization.load_pem_public_key(public_key_file_bytes) + return public_key.public_bytes( + encoding=serialization.Encoding.DER, format=serialization.PublicFormat.SubjectPublicKeyInfo + ) + except IOError as e: + raise OSError("IOError while reading public key from file") from e + + +def should_get_new_public_keys() -> bool: + """ + Check if new public keys should be generated. + + :return: True if new keys should be generated, False otherwise + """ + # Check if public keys already exist + sender_public_key_file = pathlib.Path(EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME) + recipient_public_key_file = pathlib.Path(EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME) + + if sender_public_key_file.exists() or recipient_public_key_file.exists(): + return False + + if not sender_public_key_file.exists() and recipient_public_key_file.exists(): + raise FileNotFoundError(f"Missing public key sender file at {EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME}") + + if not recipient_public_key_file.exists() and sender_public_key_file.exists(): + raise FileNotFoundError(f"Missing public key recipient file at {EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME}") + + return True + + +def write_public_key_pem_for_ecc_key(ecc_key_arn: str, ecc_public_key_filename: str): + """ + Write a public key PEM file for an ECC key. + + :param ecc_key_arn: The ARN of the KMS ECC key + :param ecc_public_key_filename: The filename to write the public key to + """ + # Safety check: Validate file is not present + public_key_file = pathlib.Path(ecc_public_key_filename) + if public_key_file.exists(): + raise FileExistsError("writePublicKeyPemForEccKey will not overwrite existing PEM files") + + # This code will call KMS to get the public key for the KMS ECC key. + # You must have kms:GetPublicKey permissions on the key for this to succeed. + # The public key will be written to the file EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME + # or EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME. + kms_client = boto3.client("kms") + response = kms_client.get_public_key(KeyId=ecc_key_arn) + public_key_bytes = response["PublicKey"] + + # Write the public key to a PEM file + public_key = serialization.load_der_public_key(public_key_bytes) + pem_data = public_key.public_bytes( + encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo + ) + + with open(ecc_public_key_filename, "wb") as f: + f.write(pem_data) diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_ecdh_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_ecdh_keyring_example/with_encrypted_client.py similarity index 85% rename from Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_ecdh_keyring_example.py rename to Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_ecdh_keyring_example/with_encrypted_client.py index 3af215b1f..9965eeb73 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_ecdh_keyring_example.py +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_ecdh_keyring_example/with_encrypted_client.py @@ -1,9 +1,10 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 """ -These examples set up DynamoDb Encryption for the AWS SDK client using the AWS KMS ECDH Keyring. +Examples demonstrating DynamoDb Encryption using a AWS KMS ECDH Keyring with EncryptedClient. -This keyring, depending on its KeyAgreement scheme, +These examples set up DynamoDb Encryption for the AWS SDK client +using the AWS KMS ECDH Keyring. This keyring, depending on its KeyAgreement scheme, takes in the sender's KMS ECC Key ARN, and the recipient's ECC Public Key to derive a shared secret. The keyring uses the shared secret to derive a data key to protect the data keys that encrypt and decrypt DynamoDb table items. @@ -16,7 +17,6 @@ - Sort key is named "sort_key" with type (S) """ -import pathlib import boto3 from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders @@ -39,13 +39,15 @@ from aws_dbesdk_dynamodb.structures.structured_encryption import ( CryptoAction, ) -from cryptography.hazmat.primitives import serialization -EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME = "KmsEccKeyringKeyringExamplePublicKeySender.pem" -EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME = "KmsEccKeyringKeyringExamplePublicKeyRecipient.pem" +from .utility import ( + EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME, + EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME, + load_public_key_bytes, +) -def kms_ecdh_keyring_get_item_put_item( +def kms_ecdh_keyring_client_example( ddb_table_name: str, ecc_key_arn: str, ecc_public_key_sender_filename: str = EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME, @@ -143,7 +145,7 @@ def kms_ecdh_keyring_get_item_put_item( put_get_item_with_keyring(kms_ecdh_keyring, ddb_table_name) -def kms_ecdh_discovery_get_item(ddb_table_name: str, ecc_recipient_key_arn: str): +def kms_ecdh_discovery_client_example(ddb_table_name: str, ecc_recipient_key_arn: str): """ Demonstrate using a KMS ECDH keyring with discovery. @@ -379,73 +381,3 @@ def put_get_item_with_keyring(aws_kms_ecdh_keyring: IKeyring, ddb_table_name: st assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200 returned_item = get_response["Item"] assert returned_item["sensitive_data"]["S"] == "encrypt and sign me!" - - -def load_public_key_bytes(ecc_public_key_filename: str) -> bytes: - """ - Load public key bytes from a PEM file. - - :param ecc_public_key_filename: The filename containing the public key - :return: The public key bytes - """ - try: - with open(ecc_public_key_filename, "rb") as f: - public_key_file_bytes = f.read() - public_key = serialization.load_pem_public_key(public_key_file_bytes) - return public_key.public_bytes( - encoding=serialization.Encoding.DER, format=serialization.PublicFormat.SubjectPublicKeyInfo - ) - except IOError as e: - raise OSError("IOError while reading public key from file") from e - - -def should_get_new_public_keys() -> bool: - """ - Check if new public keys should be generated. - - :return: True if new keys should be generated, False otherwise - """ - # Check if public keys already exist - sender_public_key_file = pathlib.Path(EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME) - recipient_public_key_file = pathlib.Path(EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME) - - if sender_public_key_file.exists() or recipient_public_key_file.exists(): - return False - - if not sender_public_key_file.exists() and recipient_public_key_file.exists(): - raise FileNotFoundError(f"Missing public key sender file at {EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME}") - - if not recipient_public_key_file.exists() and sender_public_key_file.exists(): - raise FileNotFoundError(f"Missing public key recipient file at {EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME}") - - return True - - -def write_public_key_pem_for_ecc_key(ecc_key_arn: str, ecc_public_key_filename: str): - """ - Write a public key PEM file for an ECC key. - - :param ecc_key_arn: The ARN of the KMS ECC key - :param ecc_public_key_filename: The filename to write the public key to - """ - # Safety check: Validate file is not present - public_key_file = pathlib.Path(ecc_public_key_filename) - if public_key_file.exists(): - raise FileExistsError("writePublicKeyPemForEccKey will not overwrite existing PEM files") - - # This code will call KMS to get the public key for the KMS ECC key. - # You must have kms:GetPublicKey permissions on the key for this to succeed. - # The public key will be written to the file EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME - # or EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME. - kms_client = boto3.client("kms") - response = kms_client.get_public_key(KeyId=ecc_key_arn) - public_key_bytes = response["PublicKey"] - - # Write the public key to a PEM file - public_key = serialization.load_der_public_key(public_key_bytes) - pem_data = public_key.public_bytes( - encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo - ) - - with open(ecc_public_key_filename, "wb") as f: - f.write(pem_data) diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_ecdh_keyring_example/with_encrypted_table.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_ecdh_keyring_example/with_encrypted_table.py new file mode 100644 index 000000000..266776fa6 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_ecdh_keyring_example/with_encrypted_table.py @@ -0,0 +1,389 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Examples demonstrating DynamoDb Encryption using an AWS KMS ECDH Keyring with EncryptedTable. + +These examples set up DynamoDb Encryption for the AWS SDK client +using the AWS KMS ECDH Keyring. This keyring, depending on its KeyAgreement scheme, +takes in the sender's KMS ECC Key ARN, and the recipient's ECC Public Key to derive a shared secret. +The keyring uses the shared secret to derive a data key to protect the +data keys that encrypt and decrypt DynamoDb table items. + +Running these examples require access to the DDB Table whose name +is provided in CLI arguments. +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 (S) +""" + + +import boto3 +from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders +from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig +from aws_cryptographic_material_providers.mpl.models import ( + CreateAwsKmsEcdhKeyringInput, + DBEAlgorithmSuiteId, + KmsEcdhStaticConfigurationsKmsPrivateKeyToStaticPublicKey, + KmsEcdhStaticConfigurationsKmsPublicKeyDiscovery, + KmsPrivateKeyToStaticPublicKeyInput, + KmsPublicKeyDiscoveryInput, +) +from aws_cryptographic_material_providers.mpl.references import IKeyring +from aws_cryptography_primitives.smithygenerated.aws_cryptography_primitives.models import ECDHCurveSpec +from aws_dbesdk_dynamodb.encrypted.table import EncryptedTable +from aws_dbesdk_dynamodb.structures.dynamodb import ( + DynamoDbTableEncryptionConfig, + DynamoDbTablesEncryptionConfig, +) +from aws_dbesdk_dynamodb.structures.structured_encryption import ( + CryptoAction, +) + +from .utility import ( + EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME, + EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME, + load_public_key_bytes, +) + + +def kms_ecdh_keyring_table_example( + ddb_table_name: str, + ecc_key_arn: str, + ecc_public_key_sender_filename: str = EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME, + ecc_public_key_recipient_filename: str = EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME, +): + """ + Demonstrate using a KMS ECDH keyring with static keys. + + This example takes in the sender's KMS ECC key ARN, the sender's public key, + the recipient's public key, and the algorithm definition where the ECC keys lie. + The ecc_key_arn parameter takes in the sender's KMS ECC key ARN, + the ecc_public_key_sender_filename parameter takes in the sender's public key that corresponds to the + ecc_key_arn, the ecc_public_key_recipient_filename parameter takes in the recipient's public key, + and the Curve Specification where the keys lie. + + Both public keys MUST be UTF8 PEM-encoded X.509 public key, also known as SubjectPublicKeyInfo (SPKI) + + This example encrypts a test item using the provided ECC keys and puts the + encrypted item to the provided DynamoDb table. Then, it gets the + item from the table and decrypts it. + + Running this example requires access to the DDB Table whose name + is provided in CLI arguments. + 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 (S) + This example also requires access to a KMS ECC key. + Our tests provide a KMS ECC Key ARN that anyone can use, but you + can also provide your own KMS ECC key. + To use your own KMS ECC key, you must have either: + - Its public key downloaded in a UTF-8 encoded PEM file + - kms:GetPublicKey permissions on that key. + If you do not have the public key downloaded, running this example + through its main method will download the public key for you + by calling kms:GetPublicKey. + You must also have kms:DeriveSharedSecret permissions on the KMS ECC key. + This example also requires a recipient ECC Public Key that lies on the same + curve as the sender public key. This examples uses another distinct + KMS ECC Public Key, it does not have to be a KMS key; it can be a + valid SubjectPublicKeyInfo (SPKI) Public Key. + + :param ddb_table_name: The name of the DynamoDB table + :param ecc_key_arn: The ARN of the KMS ECC key to use + :param ecc_public_key_sender_filename: The filename containing the sender's public key + :param ecc_public_key_recipient_filename: The filename containing the recipient's public key + """ + # Load UTF-8 encoded public key PEM files as DER encoded bytes. + # You may provide your own PEM files to use here. If you provide this, it MUST + # be a key on curve P256. + # If not, the main method in this class will call + # the KMS ECC key, retrieve its public key, and store it + # in a PEM file for example use. + public_key_recipient_bytes = load_public_key_bytes(ecc_public_key_recipient_filename) + public_key_sender_bytes = load_public_key_bytes(ecc_public_key_sender_filename) + + # Create a KMS ECDH keyring. + # This keyring uses the KmsPrivateKeyToStaticPublicKey configuration. This configuration calls for both of + # the keys to be on the same curve (P256, P384, P521). + # On encrypt, the keyring calls AWS KMS to derive the shared secret from the sender's KMS ECC Key ARN + # and the recipient's public key. + # For this example, on decrypt, the keyring calls AWS KMS to derive the shared secret from the + # sender's KMS ECC Key ARN and the recipient's public key; + # however, on decrypt, the recipient can construct a keyring such that the shared secret is calculated with + # the recipient's private key and the sender's public key. In both scenarios the shared secret will be the same. + # For more information on this configuration see: + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/use-kms-ecdh-keyring.html#kms-ecdh-create + # The DynamoDb encryption client uses this keyring to encrypt and decrypt items. + # This keyring takes in: + # - kms_client + # - kms_key_id: Must be an ARN representing a KMS ECC key meant for KeyAgreement + # - curve_spec: The curve name where the public keys lie + # - sender_public_key: A ByteBuffer of a UTF-8 encoded public + # key for the key passed into kms_key_id in DER format + # - recipient_public_key: A ByteBuffer of a UTF-8 encoded public + # key for the key passed into kms_key_id in DER format + mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig()) + + keyring_input = CreateAwsKmsEcdhKeyringInput( + kms_client=boto3.client("kms"), + curve_spec=ECDHCurveSpec.ECC_NIST_P256, + key_agreement_scheme=KmsEcdhStaticConfigurationsKmsPrivateKeyToStaticPublicKey( + KmsPrivateKeyToStaticPublicKeyInput( + sender_kms_identifier=ecc_key_arn, + # Must be a DER-encoded X.509 public key + sender_public_key=public_key_sender_bytes, + # Must be a DER-encoded X.509 public key + recipient_public_key=public_key_recipient_bytes, + ) + ), + ) + + kms_ecdh_keyring = mat_prov.create_aws_kms_ecdh_keyring(input=keyring_input) + + put_get_item_with_keyring(kms_ecdh_keyring, ddb_table_name) + + +def kms_ecdh_discovery_table_example(ddb_table_name: str, ecc_recipient_key_arn: str): + """ + Demonstrate using a KMS ECDH keyring with discovery. + + This example takes in the recipient's KMS ECC key ARN via + the ecc_recipient_key_arn parameter. + + This example attempts to decrypt a test item using the provided ecc_recipient_key_arn, + it does so by checking if the message header contains the recipient's public key. + + Running this example requires access to the DDB Table whose name + is provided in CLI arguments. + 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 (S) + This example also requires access to a KMS ECC key. + Our tests provide a KMS ECC Key ARN that anyone can use, but you + can also provide your own KMS ECC key. + To use your own KMS ECC key, you must have: + - kms:GetPublicKey permissions on that key. + This example will call kms:GetPublicKey on keyring creation. + You must also have kms:DeriveSharedSecret permissions on the KMS ECC key. + + :param ddb_table_name: The name of the DynamoDB table + :param ecc_recipient_key_arn: The ARN of the recipient's KMS ECC key + """ + # Create a KMS ECDH keyring. + # This keyring uses the KmsPublicKeyDiscovery configuration. + # On encrypt, the keyring will fail as it is not allowed to encrypt data under this configuration. + # On decrypt, the keyring will check if its corresponding public key is stored in the message header. It + # will AWS KMS to derive the shared from the recipient's KMS ECC Key ARN and the sender's public key; + # For more information on this configuration see: + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/use-kms-ecdh-keyring.html#kms-ecdh-discovery + # The DynamoDb encryption client uses this to encrypt and decrypt items. + # This keyring takes in: + # - kms_client + # - recipient_kms_identifier: Must be an ARN representing a KMS ECC key meant for KeyAgreement + # - curve_spec: The curve name where the public keys lie + mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig()) + + keyring_input = CreateAwsKmsEcdhKeyringInput( + kms_client=boto3.client("kms"), + curve_spec=ECDHCurveSpec.ECC_NIST_P256, + key_agreement_scheme=KmsEcdhStaticConfigurationsKmsPublicKeyDiscovery( + KmsPublicKeyDiscoveryInput(recipient_kms_identifier=ecc_recipient_key_arn) + ), + ) + + kms_ecdh_keyring = mat_prov.create_aws_kms_ecdh_keyring(input=keyring_input) + + get_item_with_keyring(kms_ecdh_keyring, ddb_table_name) + + +def get_item_with_keyring(kms_ecdh_keyring: IKeyring, ddb_table_name: str): + """ + Demonstrate get operation with a KMS ECDH keyring. + + :param kms_ecdh_keyring: The KMS ECDH keyring to use + :param ddb_table_name: The name of the DynamoDB table + """ + # 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 + attribute_actions = { + "partition_key": CryptoAction.SIGN_ONLY, # Our partition attribute must be SIGN_ONLY + "sort_key": CryptoAction.SIGN_ONLY, # Our sort attribute must be SIGN_ONLY + "sensitive_data": CryptoAction.ENCRYPT_AND_SIGN, + } + + # Configure which attributes we expect to be included in the signature + # when reading items. There are two options for configuring this: + # + # - (Recommended) Configure `allowed_unsigned_attribute_prefix`: + # 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 `attribute_actions` + # 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 `allowed_unsigned_attributes`: 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 currently authenticate all attributes. To make it easier to + # add unauthenticated attributes in the future, we define a prefix ":" for such attributes. + unsign_attr_prefix = ":" + + # Create the DynamoDb Encryption configuration for the table we will be writing to. + # Note: To use the KMS RSA keyring, your table config must specify an algorithmSuite + # that does not use asymmetric signing. + table_config = DynamoDbTableEncryptionConfig( + logical_table_name=ddb_table_name, + partition_key_name="partition_key", + sort_key_name="sort_key", + attribute_actions_on_encrypt=attribute_actions, + keyring=kms_ecdh_keyring, + allowed_unsigned_attribute_prefix=unsign_attr_prefix, + # Specify algorithmSuite without asymmetric signing here + # As of v3.0.0, the only supported algorithmSuite without asymmetric signing is + # ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_SYMSIG_HMAC_SHA384. + algorithm_suite_id=DBEAlgorithmSuiteId.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_SYMSIG_HMAC_SHA384, + ) + + table_configs = {ddb_table_name: table_config} + tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) + + # Create the EncryptedTable + ddb_table = boto3.resource("dynamodb").Table(ddb_table_name) + encrypted_table = EncryptedTable( + table=ddb_table, + encryption_config=tables_config, + ) + + # Get the item back from our table using the above table. + # The table will decrypt the item client-side using the ECDH keyring + # and return the original item. + key_to_get = {"partition_key": "awsKmsEcdhKeyringItem", "sort_key": 0} + + get_response = encrypted_table.get_item(Key=key_to_get) + + # Demonstrate that GetItem succeeded and returned the decrypted item + assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + returned_item = get_response["Item"] + assert returned_item["sensitive_data"] == "encrypt and sign me!" + + +def put_get_item_with_keyring(aws_kms_ecdh_keyring: IKeyring, ddb_table_name: str): + """ + Demonstrate put and get operations with a KMS ECDH keyring. + + :param aws_kms_ecdh_keyring: The KMS ECDH keyring to use + :param ddb_table_name: The name of the DynamoDB table + """ + # 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 + attribute_actions = { + "partition_key": CryptoAction.SIGN_ONLY, # Our partition attribute must be SIGN_ONLY + "sort_key": CryptoAction.SIGN_ONLY, # Our sort attribute must be SIGN_ONLY + "sensitive_data": CryptoAction.ENCRYPT_AND_SIGN, + } + + # Configure which attributes we expect to be included in the signature + # when reading items. There are two options for configuring this: + # + # - (Recommended) Configure `allowed_unsigned_attribute_prefix`: + # 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 `attribute_actions` + # 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 `allowed_unsigned_attributes`: 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 currently authenticate all attributes. To make it easier to + # add unauthenticated attributes in the future, we define a prefix ":" for such attributes. + unsign_attr_prefix = ":" + + # Create the DynamoDb Encryption configuration for the table we will be writing to. + # Note: To use the KMS RSA keyring, your table config must specify an algorithmSuite + # that does not use asymmetric signing. + table_config = DynamoDbTableEncryptionConfig( + logical_table_name=ddb_table_name, + partition_key_name="partition_key", + sort_key_name="sort_key", + attribute_actions_on_encrypt=attribute_actions, + keyring=aws_kms_ecdh_keyring, + allowed_unsigned_attribute_prefix=unsign_attr_prefix, + # Specify algorithmSuite without asymmetric signing here + # As of v3.0.0, the only supported algorithmSuite without asymmetric signing is + # ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_SYMSIG_HMAC_SHA384. + algorithm_suite_id=DBEAlgorithmSuiteId.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_SYMSIG_HMAC_SHA384, + ) + + table_configs = {ddb_table_name: table_config} + tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) + + # Create the EncryptedTable + ddb_table = boto3.resource("dynamodb").Table(ddb_table_name) + encrypted_table = EncryptedTable( + table=ddb_table, + encryption_config=tables_config, + ) + + # Put an item into our table using the above table. + # Before the item gets sent to DynamoDb, it will be encrypted + # client-side, according to our configuration. + item = { + "partition_key": "awsKmsEcdhKeyringItem", + "sort_key": 0, + "sensitive_data": "encrypt and sign me!", + } + + put_response = encrypted_table.put_item(Item=item) + + # Demonstrate that PutItem succeeded + assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # Get the item back from our table using the table + # The table will decrypt the item client-side using the RSA keyring + # and return the original item. + key_to_get = {"partition_key": "awsKmsEcdhKeyringItem", "sort_key": 0} + + get_response = encrypted_table.get_item(Key=key_to_get) + + # Demonstrate that GetItem succeeded and returned the decrypted item + assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + returned_item = get_response["Item"] + assert returned_item["sensitive_data"] == "encrypt and sign me!" diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_rsa_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_rsa_keyring_example.py deleted file mode 100644 index dd18e9e48..000000000 --- a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_rsa_keyring_example.py +++ /dev/null @@ -1,230 +0,0 @@ -# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: Apache-2.0 -""" -Example demonstrating DynamoDb Encryption using a KMS RSA Keyring. - -The KMS RSA Keyring uses a KMS RSA key pair to encrypt and decrypt records. The client -uses the downloaded public key to encrypt items it adds to the table. The keyring -uses the private key to decrypt existing table items it retrieves by calling -KMS' decrypt API. - -Running this example requires access to the DDB Table whose name is provided -in CLI arguments. 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 (S) - -The example also requires access to a KMS RSA key. Our tests provide a KMS RSA -ARN that anyone can use, but you can also provide your own KMS RSA key. -To use your own KMS RSA key, you must have either: - - Its public key downloaded in a UTF-8 encoded PEM file - - kms:GetPublicKey permissions on that key - -If you do not have the public key downloaded, running this example through its -main method will download the public key for you by calling kms:GetPublicKey. -You must also have kms:Decrypt permissions on the KMS RSA key. -""" - -import os - -import boto3 -from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders -from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig -from aws_cryptographic_material_providers.mpl.models import ( - CreateAwsKmsRsaKeyringInput, - DBEAlgorithmSuiteId, -) -from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient -from aws_dbesdk_dynamodb.structures.dynamodb import ( - DynamoDbTableEncryptionConfig, - DynamoDbTablesEncryptionConfig, -) -from aws_dbesdk_dynamodb.structures.structured_encryption import ( - CryptoAction, -) -from cryptography.hazmat.primitives import serialization - -DEFAULT_EXAMPLE_RSA_PUBLIC_KEY_FILENAME = "KmsRsaKeyringExamplePublicKey.pem" - - -def kms_rsa_keyring_example( - ddb_table_name: str, rsa_key_arn: str, rsa_public_key_filename: str = DEFAULT_EXAMPLE_RSA_PUBLIC_KEY_FILENAME -): - """ - Create a KMS RSA keyring and use it to encrypt/decrypt DynamoDB items. - - :param ddb_table_name: The name of the DynamoDB table - :param rsa_key_arn: ARN of the KMS RSA key - :param rsa_public_key_filename: Path to the public key PEM file - """ - # 1. Load UTF-8 encoded public key PEM file. - # You may have an RSA public key file already defined. - # If not, the main method in this class will call - # the KMS RSA key, retrieve its public key, and store it - # in a PEM file for example use. - try: - with open(rsa_public_key_filename, "rb") as f: - public_key_utf8_encoded = f.read() - except IOError as e: - raise RuntimeError("IOError while reading public key from file") from e - - # 2. Create a KMS RSA keyring. - # This keyring takes in: - # - kms_client - # - kms_key_id: Must be an ARN representing a KMS RSA key - # - public_key: A ByteBuffer of a UTF-8 encoded PEM file representing the public - # key for the key passed into kms_key_id - # - encryption_algorithm: Must be either RSAES_OAEP_SHA_256 or RSAES_OAEP_SHA_1 - mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig()) - - keyring_input = CreateAwsKmsRsaKeyringInput( - kms_key_id=rsa_key_arn, - kms_client=boto3.client("kms"), - public_key=public_key_utf8_encoded, - encryption_algorithm="RSAES_OAEP_SHA_256", - ) - - kms_rsa_keyring = mat_prov.create_aws_kms_rsa_keyring(input=keyring_input) - - # 3. 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 - attribute_actions = { - "partition_key": CryptoAction.SIGN_ONLY, # Our partition attribute must be SIGN_ONLY - "sort_key": CryptoAction.SIGN_ONLY, # Our sort attribute must be SIGN_ONLY - "sensitive_data": CryptoAction.ENCRYPT_AND_SIGN, - } - - # 4. Configure which attributes we expect to be included in the signature - # when reading items. There are two options for configuring this: - # - # - (Recommended) Configure `allowed_unsigned_attribute_prefix`: - # 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 `attribute_actions` - # 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 `allowed_unsigned_attributes`: 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 currently authenticate all attributes. To make it easier to - # add unauthenticated attributes in the future, we define a prefix ":" for such attributes. - unsign_attr_prefix = ":" - - # 5. Create the DynamoDb Encryption configuration for the table we will be writing to. - # Note: To use the KMS RSA keyring, your table config must specify an algorithmSuite - # that does not use asymmetric signing. - table_config = DynamoDbTableEncryptionConfig( - logical_table_name=ddb_table_name, - partition_key_name="partition_key", - sort_key_name="sort_key", - attribute_actions_on_encrypt=attribute_actions, - keyring=kms_rsa_keyring, - allowed_unsigned_attribute_prefix=unsign_attr_prefix, - # Specify algorithmSuite without asymmetric signing here - # As of v3.0.0, the only supported algorithmSuite without asymmetric signing is - # ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_SYMSIG_HMAC_SHA384. - algorithm_suite_id=DBEAlgorithmSuiteId.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_SYMSIG_HMAC_SHA384, - ) - - table_configs = {ddb_table_name: table_config} - tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) - - # 6. Create the EncryptedClient - ddb_client = boto3.client("dynamodb") - encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config) - - # 7. Put an item into our table using the above client. - # Before the item gets sent to DynamoDb, it will be encrypted - # client-side using the KMS RSA keyring. - item = { - "partition_key": {"S": "awsKmsRsaKeyringItem"}, - "sort_key": {"N": "0"}, - "sensitive_data": {"S": "encrypt and sign me!"}, - } - - put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item) - - # Demonstrate that PutItem succeeded - assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 - - # 8. Get the item back from our table using the client. - # The client will decrypt the item client-side using the RSA keyring - # and return the original item. - key_to_get = {"partition_key": {"S": "awsKmsRsaKeyringItem"}, "sort_key": {"N": "0"}} - - get_response = encrypted_ddb_client.get_item(TableName=ddb_table_name, Key=key_to_get) - - # Demonstrate that GetItem succeeded and returned the decrypted item - assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200 - returned_item = get_response["Item"] - assert returned_item["sensitive_data"]["S"] == "encrypt and sign me!" - - -def should_get_new_public_key(rsa_public_key_filename: str = DEFAULT_EXAMPLE_RSA_PUBLIC_KEY_FILENAME) -> bool: - """ - Check if we need to get a new public key. - - :param rsa_public_key_filename: Path to the public key PEM file - :return: True if we need to get a new public key, False otherwise - """ - # Check if a public key file already exists - public_key_file = os.path.exists(rsa_public_key_filename) - - # If a public key file already exists: do not overwrite existing file - if public_key_file: - return False - - # If file is not present, generate a new key pair - return True - - -def write_public_key_pem_for_rsa_key( - rsa_key_arn: str, rsa_public_key_filename: str = DEFAULT_EXAMPLE_RSA_PUBLIC_KEY_FILENAME -): - """ - Get the public key from KMS and write it to a PEM file. - - :param rsa_key_arn: The ARN of the KMS RSA key - :param rsa_public_key_filename: Path to write the public key PEM file - """ - # Safety check: Validate file is not present - if os.path.exists(rsa_public_key_filename): - raise RuntimeError("getRsaPublicKey will not overwrite existing PEM files") - - # This code will call KMS to get the public key for the KMS RSA key. - # You must have kms:GetPublicKey permissions on the key for this to succeed. - # The public key will be written to the file EXAMPLE_RSA_PUBLIC_KEY_FILENAME. - kms_client = boto3.client("kms") - response = kms_client.get_public_key(KeyId=rsa_key_arn) - public_key_bytes = response["PublicKey"] - - # Convert the public key to PEM format - public_key = serialization.load_der_public_key(public_key_bytes) - pem_data = public_key.public_bytes( - encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo - ) - - # Write the PEM file - try: - with open(rsa_public_key_filename, "wb") as f: - f.write(pem_data) - except IOError as e: - raise RuntimeError("IOError while writing public key PEM") from e diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_rsa_keyring_example/__init__.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_rsa_keyring_example/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_rsa_keyring_example/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_rsa_keyring_example/encryption_config.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_rsa_keyring_example/encryption_config.py new file mode 100644 index 000000000..e960b1897 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_rsa_keyring_example/encryption_config.py @@ -0,0 +1,114 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Configuration module for KMS RSA keyring encryption setup. + +This module provides the common encryption configuration used by both +EncryptedClient and EncryptedTable examples. +""" + +import boto3 +from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders +from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig +from aws_cryptographic_material_providers.mpl.models import ( + CreateAwsKmsRsaKeyringInput, + DBEAlgorithmSuiteId, +) +from aws_dbesdk_dynamodb.structures.dynamodb import ( + DynamoDbTableEncryptionConfig, + DynamoDbTablesEncryptionConfig, +) +from aws_dbesdk_dynamodb.structures.structured_encryption import ( + CryptoAction, +) + + +def create_encryption_config( + ddb_table_name: str, rsa_key_arn: str, public_key_utf8_encoded: bytes +) -> DynamoDbTablesEncryptionConfig: + """ + Create the encryption configuration for DynamoDB encryption using KMS RSA keyring. + + :param ddb_table_name: The name of the DynamoDB table + :param rsa_key_arn: ARN of the KMS RSA key + :param public_key_utf8_encoded: UTF-8 encoded public key in PEM format + :return: The DynamoDB tables encryption configuration + """ + # 1. Create a KMS RSA keyring. + # This keyring takes in: + # - kms_client + # - kms_key_id: Must be an ARN representing a KMS RSA key + # - public_key: A ByteBuffer of a UTF-8 encoded PEM file representing the public + # key for the key passed into kms_key_id + # - encryption_algorithm: Must be either RSAES_OAEP_SHA_256 or RSAES_OAEP_SHA_1 + mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig()) + + keyring_input = CreateAwsKmsRsaKeyringInput( + kms_key_id=rsa_key_arn, + kms_client=boto3.client("kms"), + public_key=public_key_utf8_encoded, + encryption_algorithm="RSAES_OAEP_SHA_256", + ) + + kms_rsa_keyring = mat_prov.create_aws_kms_rsa_keyring(input=keyring_input) + + # 2. 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 + attribute_actions = { + "partition_key": CryptoAction.SIGN_ONLY, # Our partition attribute must be SIGN_ONLY + "sort_key": CryptoAction.SIGN_ONLY, # Our sort attribute must be SIGN_ONLY + "sensitive_data": CryptoAction.ENCRYPT_AND_SIGN, + } + + # 3. Configure which attributes we expect to be included in the signature + # when reading items. There are two options for configuring this: + # + # - (Recommended) Configure `allowed_unsigned_attribute_prefix`: + # 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 `attribute_actions` + # 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 `allowed_unsigned_attributes`: 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 currently authenticate all attributes. To make it easier to + # add unauthenticated attributes in the future, we define a prefix ":" for such attributes. + unsign_attr_prefix = ":" + + # 4. Create the DynamoDb Encryption configuration for the table we will be writing to. + # Note: To use the KMS RSA keyring, your table config must specify an algorithmSuite + # that does not use asymmetric signing. + table_config = DynamoDbTableEncryptionConfig( + logical_table_name=ddb_table_name, + partition_key_name="partition_key", + sort_key_name="sort_key", + attribute_actions_on_encrypt=attribute_actions, + keyring=kms_rsa_keyring, + allowed_unsigned_attribute_prefix=unsign_attr_prefix, + # Specify algorithmSuite without asymmetric signing here + # As of v3.0.0, the only supported algorithmSuite without asymmetric signing is + # ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_SYMSIG_HMAC_SHA384. + algorithm_suite_id=DBEAlgorithmSuiteId.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_SYMSIG_HMAC_SHA384, + ) + + table_configs = {ddb_table_name: table_config} + return DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_rsa_keyring_example/utility.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_rsa_keyring_example/utility.py new file mode 100644 index 000000000..3e9302073 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_rsa_keyring_example/utility.py @@ -0,0 +1,62 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Utility functions for the KMS RSA keyring example.""" + +import os + +import boto3 +from cryptography.hazmat.primitives import serialization + +DEFAULT_EXAMPLE_RSA_PUBLIC_KEY_FILENAME = "KmsRsaKeyringExamplePublicKey.pem" + + +def should_get_new_public_key(rsa_public_key_filename: str = DEFAULT_EXAMPLE_RSA_PUBLIC_KEY_FILENAME) -> bool: + """ + Check if we need to get a new public key. + + :param rsa_public_key_filename: Path to the public key PEM file + :return: True if we need to get a new public key, False otherwise + """ + # Check if a public key file already exists + public_key_file = os.path.exists(rsa_public_key_filename) + + # If a public key file already exists: do not overwrite existing file + if public_key_file: + return False + + # If file is not present, generate a new key pair + return True + + +def write_public_key_pem_for_rsa_key( + rsa_key_arn: str, rsa_public_key_filename: str = DEFAULT_EXAMPLE_RSA_PUBLIC_KEY_FILENAME +): + """ + Get the public key from KMS and write it to a PEM file. + + :param rsa_key_arn: The ARN of the KMS RSA key + :param rsa_public_key_filename: Path to write the public key PEM file + """ + # Safety check: Validate file is not present + if os.path.exists(rsa_public_key_filename): + raise FileExistsError("getRsaPublicKey will not overwrite existing PEM files") + + # This code will call KMS to get the public key for the KMS RSA key. + # You must have kms:GetPublicKey permissions on the key for this to succeed. + # The public key will be written to the file EXAMPLE_RSA_PUBLIC_KEY_FILENAME. + kms_client = boto3.client("kms") + response = kms_client.get_public_key(KeyId=rsa_key_arn) + public_key_bytes = response["PublicKey"] + + # Convert the public key to PEM format + public_key = serialization.load_der_public_key(public_key_bytes) + pem_data = public_key.public_bytes( + encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo + ) + + # Write the PEM file + try: + with open(rsa_public_key_filename, "wb") as f: + f.write(pem_data) + except IOError as e: + raise OSError("IOError while writing public key PEM") from e diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_rsa_keyring_example/with_encrypted_client.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_rsa_keyring_example/with_encrypted_client.py new file mode 100644 index 000000000..ab4b15332 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_rsa_keyring_example/with_encrypted_client.py @@ -0,0 +1,92 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Example demonstrating DynamoDb Encryption using a KMS RSA Keyring with EncryptedClient. + +The KMS RSA Keyring uses a KMS RSA key pair to encrypt and decrypt records. The client +uses the downloaded public key to encrypt items it adds to the table. The keyring +uses the private key to decrypt existing table items it retrieves by calling +KMS' decrypt API. + +Running this example requires access to the DDB Table whose name is provided +in CLI arguments. 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 (S) + +The example also requires access to a KMS RSA key. Our tests provide a KMS RSA +ARN that anyone can use, but you can also provide your own KMS RSA key. +To use your own KMS RSA key, you must have either: + - Its public key downloaded in a UTF-8 encoded PEM file + - kms:GetPublicKey permissions on that key + +If you do not have the public key downloaded, running this example through its +main method will download the public key for you by calling kms:GetPublicKey. +You must also have kms:Decrypt permissions on the KMS RSA key. +""" + +import boto3 +from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient + +from .encryption_config import create_encryption_config +from .utility import DEFAULT_EXAMPLE_RSA_PUBLIC_KEY_FILENAME + + +def kms_rsa_keyring_client_example( + ddb_table_name: str, rsa_key_arn: str, rsa_public_key_filename: str = DEFAULT_EXAMPLE_RSA_PUBLIC_KEY_FILENAME +): + """ + Create a KMS RSA keyring and use it to encrypt/decrypt DynamoDB items with EncryptedClient. + + :param ddb_table_name: The name of the DynamoDB table + :param rsa_key_arn: ARN of the KMS RSA key + :param rsa_public_key_filename: Path to the public key PEM file + """ + # 1. Load UTF-8 encoded public key PEM file. + # You may have an RSA public key file already defined. + # If not, the main method in this class will call + # the KMS RSA key, retrieve its public key, and store it + # in a PEM file for example use. + try: + with open(rsa_public_key_filename, "rb") as f: + public_key_utf8_encoded = f.read() + except IOError as e: + raise OSError("IOError while reading public key from file") from e + + # 2. Create the DynamoDb Encryption configuration for the table we will be writing to. + # See encryption_config.py in this directory for detailed steps on the encryption configuration. + tables_config = create_encryption_config( + ddb_table_name=ddb_table_name, + rsa_key_arn=rsa_key_arn, + public_key_utf8_encoded=public_key_utf8_encoded, + ) + + # 3. Create the EncryptedClient + ddb_client = boto3.client("dynamodb") + encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config) + + # 4. Put an item into our table using the above client. + # Before the item gets sent to DynamoDb, it will be encrypted + # client-side using the KMS RSA keyring. + item = { + "partition_key": {"S": "awsKmsRsaKeyringItem"}, + "sort_key": {"N": "0"}, + "sensitive_data": {"S": "encrypt and sign me!"}, + } + + put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item) + + # Demonstrate that PutItem succeeded + assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # 5. Get the item back from our table using the client. + # The client will decrypt the item client-side using the RSA keyring + # and return the original item. + key_to_get = {"partition_key": {"S": "awsKmsRsaKeyringItem"}, "sort_key": {"N": "0"}} + + get_response = encrypted_ddb_client.get_item(TableName=ddb_table_name, Key=key_to_get) + + # Demonstrate that GetItem succeeded and returned the decrypted item + assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + returned_item = get_response["Item"] + assert returned_item["sensitive_data"]["S"] == "encrypt and sign me!" diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_rsa_keyring_example/with_encrypted_table.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_rsa_keyring_example/with_encrypted_table.py new file mode 100644 index 000000000..2d71ec9fa --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_rsa_keyring_example/with_encrypted_table.py @@ -0,0 +1,95 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Example demonstrating DynamoDb Encryption using a KMS RSA Keyring with EncryptedTable. + +The KMS RSA Keyring uses a KMS RSA key pair to encrypt and decrypt records. The client +uses the downloaded public key to encrypt items it adds to the table. The keyring +uses the private key to decrypt existing table items it retrieves by calling +KMS' decrypt API. + +Running this example requires access to the DDB Table whose name is provided +in CLI arguments. 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 (S) + +The example also requires access to a KMS RSA key. Our tests provide a KMS RSA +ARN that anyone can use, but you can also provide your own KMS RSA key. +To use your own KMS RSA key, you must have either: + - Its public key downloaded in a UTF-8 encoded PEM file + - kms:GetPublicKey permissions on that key + +If you do not have the public key downloaded, running this example through its +main method will download the public key for you by calling kms:GetPublicKey. +You must also have kms:Decrypt permissions on the KMS RSA key. +""" + +import boto3 +from aws_dbesdk_dynamodb.encrypted.table import EncryptedTable + +from .encryption_config import create_encryption_config +from .utility import DEFAULT_EXAMPLE_RSA_PUBLIC_KEY_FILENAME + + +def kms_rsa_keyring_table_example( + ddb_table_name: str, rsa_key_arn: str, rsa_public_key_filename: str = DEFAULT_EXAMPLE_RSA_PUBLIC_KEY_FILENAME +): + """ + Create a KMS RSA keyring and use it to encrypt/decrypt DynamoDB items with EncryptedTable. + + :param ddb_table_name: The name of the DynamoDB table + :param rsa_key_arn: ARN of the KMS RSA key + :param rsa_public_key_filename: Path to the public key PEM file + """ + # 1. Load UTF-8 encoded public key PEM file. + # You may have an RSA public key file already defined. + # If not, the main method in this class will call + # the KMS RSA key, retrieve its public key, and store it + # in a PEM file for example use. + try: + with open(rsa_public_key_filename, "rb") as f: + public_key_utf8_encoded = f.read() + except IOError as e: + raise OSError("IOError while reading public key from file") from e + + # 2. Create the DynamoDb Encryption configuration for the table we will be writing to. + # See encryption_config.py in this directory for detailed steps on the encryption configuration. + tables_config = create_encryption_config( + ddb_table_name=ddb_table_name, + rsa_key_arn=rsa_key_arn, + public_key_utf8_encoded=public_key_utf8_encoded, + ) + + # 3. Create the EncryptedTable + ddb_table = boto3.resource("dynamodb").Table(ddb_table_name) + encrypted_table = EncryptedTable( + table=ddb_table, + encryption_config=tables_config, + ) + + # 4. Put an item into our table using the above table. + # Before the item gets sent to DynamoDb, it will be encrypted + # client-side using the KMS RSA keyring. + item = { + "partition_key": "awsKmsRsaKeyringItem", + "sort_key": 0, + "sensitive_data": "encrypt and sign me!", + } + + put_response = encrypted_table.put_item(Item=item) + + # Demonstrate that PutItem succeeded + assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # 5. Get the item back from our table using the table. + # The table will decrypt the item client-side using the RSA keyring + # and return the original item. + key_to_get = {"partition_key": "awsKmsRsaKeyringItem", "sort_key": 0} + + get_response = encrypted_table.get_item(Key=key_to_get) + + # Demonstrate that GetItem succeeded and returned the decrypted item + assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + returned_item = get_response["Item"] + assert returned_item["sensitive_data"] == "encrypt and sign me!" diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/mrk_discovery_multi_keyring_example/__init__.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/mrk_discovery_multi_keyring_example/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/mrk_discovery_multi_keyring_example/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/mrk_discovery_multi_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/mrk_discovery_multi_keyring_example/with_encrypted_client.py similarity index 98% rename from Examples/runtimes/python/DynamoDBEncryption/src/keyring/mrk_discovery_multi_keyring_example.py rename to Examples/runtimes/python/DynamoDBEncryption/src/keyring/mrk_discovery_multi_keyring_example/with_encrypted_client.py index 7d6c77357..0b3a3465f 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/mrk_discovery_multi_keyring_example.py +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/mrk_discovery_multi_keyring_example/with_encrypted_client.py @@ -1,7 +1,7 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 """ -Example demonstrating DynamoDb Encryption using a MRK discovery multi-keyring. +Example demonstrating DynamoDb Encryption using a MRK discovery multi-keyring with EncryptedClient. A discovery keyring is not provided with any wrapping keys; instead, it recognizes the KMS key that was used to encrypt a data key, and asks KMS to decrypt with that @@ -40,11 +40,11 @@ ) -def multi_mrk_discovery_keyring_get_item_put_item( +def multi_mrk_discovery_keyring_client_example( ddb_table_name: str, key_arn: str, account_ids: List[str], regions: List[str] ): """ - Demonstrate using a MRK discovery multi-keyring. + Demonstrate using a MRK discovery multi-keyring with EncryptedClient. :param ddb_table_name: The name of the DynamoDB table :param key_arn: The ARN of the KMS key to use for encryption diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/mrk_discovery_multi_keyring_example/with_encrypted_table.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/mrk_discovery_multi_keyring_example/with_encrypted_table.py new file mode 100644 index 000000000..849b5a14b --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/mrk_discovery_multi_keyring_example/with_encrypted_table.py @@ -0,0 +1,190 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Example demonstrating DynamoDb Encryption using a MRK discovery multi-keyring with EncryptedTable. + +A discovery keyring is not provided with any wrapping keys; instead, it recognizes +the KMS key that was used to encrypt a data key, and asks KMS to decrypt with that +KMS key. Discovery keyrings cannot be used to encrypt data. + +For more information on discovery keyrings, see: +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/use-kms-keyring.html#kms-keyring-discovery + +The example encrypts an item using an MRK multi-keyring and puts the encrypted +item to the configured DynamoDb table. Then, it gets the item from the table and +decrypts it using the discovery keyring. + +Running this example requires access to the DDB Table whose name is provided in +CLI arguments. 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 (S) +""" +from typing import List + +import boto3 +from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders +from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig +from aws_cryptographic_material_providers.mpl.models import ( + CreateAwsKmsMrkDiscoveryMultiKeyringInput, + CreateAwsKmsMrkMultiKeyringInput, + DiscoveryFilter, +) +from aws_dbesdk_dynamodb.encrypted.table import EncryptedTable +from aws_dbesdk_dynamodb.structures.dynamodb import ( + DynamoDbTableEncryptionConfig, + DynamoDbTablesEncryptionConfig, +) +from aws_dbesdk_dynamodb.structures.structured_encryption import ( + CryptoAction, +) + + +def multi_mrk_discovery_keyring_table_example( + ddb_table_name: str, key_arn: str, account_ids: List[str], regions: List[str] +): + """ + Demonstrate using a MRK discovery multi-keyring with EncryptedTable. + + :param ddb_table_name: The name of the DynamoDB table + :param key_arn: The ARN of the KMS key to use for encryption + :param account_ids: List of AWS account IDs for discovery filter + :param regions: List of AWS regions for discovery keyring + """ + # 1. Create a single MRK multi-keyring using the key arn. + # Although this example demonstrates use of the MRK discovery multi-keyring, + # a discovery keyring cannot be used to encrypt. So we will need to construct + # a non-discovery keyring for this example to encrypt. For more information on MRK + # multi-keyrings, see the MultiMrkKeyringExample in this directory. + # Though this is an "MRK multi-keyring", we do not need to provide multiple keys, + # and can use single-region KMS keys. We will provide a single key here; this + # can be either an MRK or a single-region key. + mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig()) + + encrypt_keyring = mat_prov.create_aws_kms_mrk_multi_keyring( + input=CreateAwsKmsMrkMultiKeyringInput(generator=key_arn) + ) + + # 2. 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 + attribute_actions_on_encrypt = { + "partition_key": CryptoAction.SIGN_ONLY, # Our partition attribute must be SIGN_ONLY + "sort_key": CryptoAction.SIGN_ONLY, # Our sort attribute must be SIGN_ONLY + "sensitive_data": CryptoAction.ENCRYPT_AND_SIGN, + } + + # 3. Configure which attributes we expect to be included in the signature + # when reading items. There are two options for configuring this: + # + # - (Recommended) Configure `allowed_unsigned_attribute_prefix`: + # 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 `attribute_actions_on_encrypt` + # 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 `allowed_unsigned_attributes`: 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 currently authenticate all attributes. To make it easier to + # add unauthenticated attributes in the future, we define a prefix ":" for such attributes. + unsign_attr_prefix = ":" + + # 4. Create the DynamoDb Encryption configuration for the table we will be writing to. + table_config = DynamoDbTableEncryptionConfig( + logical_table_name=ddb_table_name, + partition_key_name="partition_key", + sort_key_name="sort_key", + attribute_actions_on_encrypt=attribute_actions_on_encrypt, + keyring=encrypt_keyring, + allowed_unsigned_attribute_prefix=unsign_attr_prefix, + ) + + table_configs = {ddb_table_name: table_config} + tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) + + # 5. Create the EncryptedTable + ddb_table = boto3.resource("dynamodb").Table(ddb_table_name) + encrypted_table = EncryptedTable( + table=ddb_table, + encryption_config=tables_config, + ) + # 6. Put an item into our table using the above table. + # Before the item gets sent to DynamoDb, it will be encrypted + # client-side using the MRK multi-keyring. + item = { + "partition_key": "awsKmsMrkDiscoveryMultiKeyringItem", + "sort_key": 0, + "sensitive_data": "encrypt and sign me!", + } + + put_response = encrypted_table.put_item(Item=item) + + # Demonstrate that PutItem succeeded + assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # 7. Construct a discovery filter. + # A discovery filter limits the set of encrypted data keys + # the keyring can use to decrypt data. + # We will only let the keyring use keys in the selected AWS accounts + # and in the `aws` partition. + # This is the suggested config for most users; for more detailed config, see + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/use-kms-keyring.html#kms-keyring-discovery + discovery_filter = DiscoveryFilter(partition="aws", account_ids=account_ids) + + # 8. Construct a discovery keyring. + # Note that we choose to use the MRK discovery multi-keyring, even though + # our original keyring used a single KMS key. + decrypt_keyring = mat_prov.create_aws_kms_mrk_discovery_multi_keyring( + input=CreateAwsKmsMrkDiscoveryMultiKeyringInput(discovery_filter=discovery_filter, regions=regions) + ) + + # 9. Create new DDB config and client using the decrypt discovery keyring. + # This is the same as the above config, except we pass in the decrypt keyring. + table_config_for_decrypt = DynamoDbTableEncryptionConfig( + logical_table_name=ddb_table_name, + partition_key_name="partition_key", + sort_key_name="sort_key", + attribute_actions_on_encrypt=attribute_actions_on_encrypt, + # Add decrypt keyring here + keyring=decrypt_keyring, + allowed_unsigned_attribute_prefix=unsign_attr_prefix, + ) + + table_configs_for_decrypt = {ddb_table_name: table_config_for_decrypt} + tables_config_for_decrypt = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs_for_decrypt) + + encrypted_ddb_table_for_decrypt = EncryptedTable(table=ddb_table, encryption_config=tables_config_for_decrypt) + + # 10. Get the item back from our table using the above table. + # The table will retrieve encrypted items from the DDB table, then + # detect the KMS key that was used to encrypt their data keys. + # The table will make a request to KMS to decrypt with the encrypting KMS key. + # If the table has permission to decrypt with the KMS key, + # the table will decrypt the item client-side using the keyring + # and return the original item. + key_to_get = {"partition_key": "awsKmsMrkDiscoveryMultiKeyringItem", "sort_key": 0} + + get_response = encrypted_ddb_table_for_decrypt.get_item(Key=key_to_get) + + # Demonstrate that GetItem succeeded and returned the decrypted item + assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + returned_item = get_response["Item"] + assert returned_item["sensitive_data"] == "encrypt and sign me!" diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/mrk_multi_keyring_example/__init__.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/mrk_multi_keyring_example/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/mrk_multi_keyring_example/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/mrk_multi_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/mrk_multi_keyring_example/with_encrypted_client.py similarity index 98% rename from Examples/runtimes/python/DynamoDBEncryption/src/keyring/mrk_multi_keyring_example.py rename to Examples/runtimes/python/DynamoDBEncryption/src/keyring/mrk_multi_keyring_example/with_encrypted_client.py index dbc774763..ae801e0dd 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/mrk_multi_keyring_example.py +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/mrk_multi_keyring_example/with_encrypted_client.py @@ -56,9 +56,9 @@ ) -def multi_mrk_keyring_get_item_put_item(ddb_table_name: str, mrk_key_arn: str, key_arn: str, mrk_replica_key_arn: str): +def multi_mrk_keyring_client_example(ddb_table_name: str, mrk_key_arn: str, key_arn: str, mrk_replica_key_arn: str): """ - Demonstrate using a MRK multi-keyring. + Demonstrate using a MRK multi-keyring with EncryptedClient. :param ddb_table_name: The name of the DynamoDB table :param mrk_key_arn: The ARN of the MRK key to use as generator diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/mrk_multi_keyring_example/with_encrypted_table.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/mrk_multi_keyring_example/with_encrypted_table.py new file mode 100644 index 000000000..d477f51fe --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/mrk_multi_keyring_example/with_encrypted_table.py @@ -0,0 +1,250 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Example demonstrating DynamoDb Encryption using an MRK multi-keyring configuration. + +The MRK multi-keyring accepts multiple AWS KMS MRKs (multi-region keys) or regular +AWS KMS keys (single-region keys) and uses them to encrypt and decrypt data. Data +encrypted using an MRK multi-keyring can be decrypted using any of its component +keys. If a component key is an MRK with a replica in a second region, the replica +key can also be used to decrypt data. + +For more information on MRKs and multi-keyrings, see: +- MRKs: https://docs.aws.amazon.com/kms/latest/developerguide/multi-region-keys-overview.html +- Multi-keyrings: https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/use-multi-keyring.html + +The example creates a new MRK multi-keyring consisting of one MRK (labeled as the +"generator keyring") and one single-region key (labeled as the only "child keyring"). +The MRK also has a replica in a second region. + +The example encrypts a test item using the MRK multi-keyring and puts the encrypted +item to the provided DynamoDb table. Then, it gets the item from the table and +decrypts it using three different configs: + 1. The MRK multi-keyring, where the MRK key is used to decrypt + 2. Another MRK multi-keyring, where the replica MRK key is used to decrypt + 3. Another MRK multi-keyring, where the single-region key that was present + in the original MRK multi-keyring is used to decrypt + +Running this example requires access to the DDB Table whose name is provided in +CLI arguments. 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 (S) + +Since this example demonstrates multi-region use cases, it requires a default +region set in your AWS client. You can set a default region through the AWS CLI: + aws configure set region [region-name] +For example: + aws configure set region us-west-2 + +For more information on using AWS CLI to set config, see: +https://awscli.amazonaws.com/v2/documentation/api/latest/reference/configure/set.html +""" +import boto3 +from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders +from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig +from aws_cryptographic_material_providers.mpl.models import ( + CreateAwsKmsMrkMultiKeyringInput, +) +from aws_dbesdk_dynamodb.encrypted.table import EncryptedTable +from aws_dbesdk_dynamodb.structures.dynamodb import ( + DynamoDbTableEncryptionConfig, + DynamoDbTablesEncryptionConfig, +) +from aws_dbesdk_dynamodb.structures.structured_encryption import ( + CryptoAction, +) + + +def multi_mrk_keyring_table_example(ddb_table_name: str, mrk_key_arn: str, key_arn: str, mrk_replica_key_arn: str): + """ + Demonstrate using a MRK multi-keyring with EncryptedTable. + + :param ddb_table_name: The name of the DynamoDB table + :param mrk_key_arn: The ARN of the MRK key to use as generator + :param key_arn: The ARN of the single-region key to use as child + :param mrk_replica_key_arn: The ARN of the MRK replica key + """ + # 1. Create a single MRK multi-keyring using the MRK arn and the single-region key arn. + mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig()) + + # Create the multi-keyring, using the MRK as the generator key, + # and the single-region key as a child key. + # Note that the generator key will generate and encrypt a plaintext data key + # and all child keys will only encrypt that same plaintext data key. + # As such, you must have permission to call KMS:GenerateDataKey on your generator key + # and permission to call KMS:Encrypt on all child keys. + # For more information, see the AWS docs on multi-keyrings above. + aws_kms_mrk_multi_keyring = mat_prov.create_aws_kms_mrk_multi_keyring( + input=CreateAwsKmsMrkMultiKeyringInput(generator=mrk_key_arn, kms_key_ids=[key_arn]) + ) + + # 2. 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 + attribute_actions_on_encrypt = { + "partition_key": CryptoAction.SIGN_ONLY, # Our partition attribute must be SIGN_ONLY + "sort_key": CryptoAction.SIGN_ONLY, # Our sort attribute must be SIGN_ONLY + "sensitive_data": CryptoAction.ENCRYPT_AND_SIGN, + } + + # 3. Configure which attributes we expect to be included in the signature + # when reading items. There are two options for configuring this: + # + # - (Recommended) Configure `allowed_unsigned_attribute_prefix`: + # 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 `attribute_actions_on_encrypt` + # 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 `allowed_unsigned_attributes`: 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 currently authenticate all attributes. To make it easier to + # add unauthenticated attributes in the future, we define a prefix ":" for such attributes. + unsign_attr_prefix = ":" + + # 4. Create the DynamoDb Encryption configuration for the table we will be writing to. + table_config = DynamoDbTableEncryptionConfig( + logical_table_name=ddb_table_name, + partition_key_name="partition_key", + sort_key_name="sort_key", + attribute_actions_on_encrypt=attribute_actions_on_encrypt, + keyring=aws_kms_mrk_multi_keyring, + allowed_unsigned_attribute_prefix=unsign_attr_prefix, + ) + + table_configs = {ddb_table_name: table_config} + tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) + + # 5. Create the EncryptedTable + ddb_table = boto3.resource("dynamodb").Table(ddb_table_name) + encrypted_table = EncryptedTable( + table=ddb_table, + encryption_config=tables_config, + ) + + # 6. Put an item into our table using the encrypted table. + # Before the item gets sent to DynamoDb, it will be encrypted + # client-side using the MRK multi-keyring. + # The data key protecting this item will be encrypted + # with all the KMS Keys in this keyring, so that it can be + # decrypted with any one of those KMS Keys. + item = { + "partition_key": "awsKmsMrkMultiKeyringItem", + "sort_key": 0, + "sensitive_data": "encrypt and sign me!", + } + + put_response = encrypted_table.put_item(Item=item) + + # Demonstrate that PutItem succeeded + assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # 7. Get the item back from our table using the encrypted table. + # The client will decrypt the item client-side using the MRK + # and return back the original item. + # Since the generator key is the first available key in the keyring, + # that is the KMS Key that will be used to decrypt this item. + key_to_get = {"partition_key": "awsKmsMrkMultiKeyringItem", "sort_key": 0} + + get_response = encrypted_table.get_item(Key=key_to_get) + + # Demonstrate that GetItem succeeded and returned the decrypted item + assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + returned_item = get_response["Item"] + assert returned_item["sensitive_data"] == "encrypt and sign me!" + + # 8. Create a MRK keyring using the replica MRK arn. + # We will use this to demonstrate that the replica MRK + # can decrypt data created with the original MRK, + # even when the replica MRK was not present in the + # encrypting multi-keyring. + only_replica_key_mrk_multi_keyring = mat_prov.create_aws_kms_mrk_multi_keyring( + input=CreateAwsKmsMrkMultiKeyringInput(kms_key_ids=[mrk_replica_key_arn]) + ) + + # 9. Create a new config and client using the MRK keyring. + # This is the same setup as above, except we provide the MRK keyring to the config. + only_replica_key_table_config = DynamoDbTableEncryptionConfig( + logical_table_name=ddb_table_name, + partition_key_name="partition_key", + sort_key_name="sort_key", + attribute_actions_on_encrypt=attribute_actions_on_encrypt, + keyring=only_replica_key_mrk_multi_keyring, # Only replica keyring added here + allowed_unsigned_attribute_prefix=unsign_attr_prefix, + ) + + only_replica_key_table_configs = {ddb_table_name: only_replica_key_table_config} + only_replica_key_tables_config = DynamoDbTablesEncryptionConfig( + table_encryption_configs=only_replica_key_table_configs + ) + + only_replica_key_encrypted_table = EncryptedTable( + table=ddb_table, + encryption_config=only_replica_key_tables_config, + ) + + # 10. Get the item back from our table using the table configured with the replica. + # The client will decrypt the item client-side using the replica MRK + # and return back the original item. + only_replica_key_get_response = only_replica_key_encrypted_table.get_item(Key=key_to_get) + + # Demonstrate that GetItem succeeded and returned the decrypted item + assert only_replica_key_get_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + only_replica_key_returned_item = only_replica_key_get_response["Item"] + assert only_replica_key_returned_item["sensitive_data"] == "encrypt and sign me!" + + # 11. Create an AWS KMS keyring using the single-region key ARN. + # We will use this to demonstrate that the single-region key + # can decrypt data created with the MRK multi-keyring, + # since it is present in the keyring used to encrypt. + only_srk_keyring = mat_prov.create_aws_kms_mrk_multi_keyring( + input=CreateAwsKmsMrkMultiKeyringInput(kms_key_ids=[key_arn]) + ) + + # 12. Create a new config and client using the AWS KMS keyring. + # This is the same setup as above, except we provide the AWS KMS keyring to the config. + only_srk_table_config = DynamoDbTableEncryptionConfig( + logical_table_name=ddb_table_name, + partition_key_name="partition_key", + sort_key_name="sort_key", + attribute_actions_on_encrypt=attribute_actions_on_encrypt, + keyring=only_srk_keyring, # Only single-region key keyring added here + allowed_unsigned_attribute_prefix=unsign_attr_prefix, + ) + + only_srk_table_configs = {ddb_table_name: only_srk_table_config} + only_srk_tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=only_srk_table_configs) + + only_srk_encrypted_table = EncryptedTable( + table=ddb_table, + encryption_config=only_srk_tables_config, + ) + + # 13. Get the item back from our table using the table configured with the single-region key. + # The client will decrypt the item client-side using the single-region key + # and return back the original item. + only_srk_get_response = only_srk_encrypted_table.get_item(Key=key_to_get) + + # Demonstrate that GetItem succeeded and returned the decrypted item + assert only_srk_get_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + only_srk_returned_item = only_srk_get_response["Item"] + assert only_srk_returned_item["sensitive_data"] == "encrypt and sign me!" diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/multi_keyring_example/__init__.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/multi_keyring_example/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/multi_keyring_example/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/multi_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/multi_keyring_example/with_encrypted_client.py similarity index 98% rename from Examples/runtimes/python/DynamoDBEncryption/src/keyring/multi_keyring_example.py rename to Examples/runtimes/python/DynamoDBEncryption/src/keyring/multi_keyring_example/with_encrypted_client.py index 5cb67df61..944dd28c8 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/multi_keyring_example.py +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/multi_keyring_example/with_encrypted_client.py @@ -1,7 +1,7 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 """ -Example demonstrating DynamoDb Encryption using a multi-keyring configuration. +Example demonstrating DynamoDb Encryption using a multi-keyring configuration with EncryptedClient. A multi-keyring accepts multiple keyrings and uses them to encrypt and decrypt data. Data encrypted with a multi-keyring can be decrypted with any of its component keyrings. @@ -45,7 +45,7 @@ ) -def multi_keyring_get_item_put_item(ddb_table_name: str, key_arn: str, aes_key_bytes: bytes): +def multi_keyring_client_example(ddb_table_name: str, key_arn: str, aes_key_bytes: bytes): """ Demonstrate using a multi-keyring. diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/multi_keyring_example/with_encrypted_table.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/multi_keyring_example/with_encrypted_table.py new file mode 100644 index 000000000..c7c3b15b4 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/multi_keyring_example/with_encrypted_table.py @@ -0,0 +1,209 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Example demonstrating DynamoDb Encryption using a multi-keyring configuration with EncryptedClient. + +A multi-keyring accepts multiple keyrings and uses them to encrypt and decrypt data. +Data encrypted with a multi-keyring can be decrypted with any of its component keyrings. + +For more information on multi-keyrings, see: +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/use-multi-keyring.html + +The example creates a multi-keyring consisting of an AWS KMS keyring (labeled the +"generator keyring") and a raw AES keyring (labeled as the only "child keyring"). +It encrypts a test item using the multi-keyring and puts the encrypted item to the +provided DynamoDb table. Then, it gets the item from the table and decrypts it +using only the raw AES keyring. + +The example takes an `aes_key_bytes` parameter representing a 256-bit AES key. +If run through the class's main method, it will create a new key. In practice, +users should not randomly generate a key, but instead retrieve an existing key +from a secure key management system (e.g. an HSM). + +Running this example requires access to the DDB Table whose name is provided in +CLI arguments. 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 (S) +""" +import boto3 +from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders +from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig +from aws_cryptographic_material_providers.mpl.models import ( + AesWrappingAlg, + CreateAwsKmsMrkMultiKeyringInput, + CreateMultiKeyringInput, + CreateRawAesKeyringInput, +) +from aws_dbesdk_dynamodb.encrypted.table import EncryptedTable +from aws_dbesdk_dynamodb.structures.dynamodb import ( + DynamoDbTableEncryptionConfig, + DynamoDbTablesEncryptionConfig, +) +from aws_dbesdk_dynamodb.structures.structured_encryption import ( + CryptoAction, +) + + +def multi_keyring_table_example(ddb_table_name: str, key_arn: str, aes_key_bytes: bytes): + """ + Demonstrate using a multi-keyring. + + :param ddb_table_name: The name of the DynamoDB table + :param key_arn: The ARN of the KMS key to use + :param aes_key_bytes: The AES key bytes to use + """ + # 1. Create the raw AES keyring. + mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig()) + + raw_aes_keyring_input = CreateRawAesKeyringInput( + key_name="my-aes-key-name", + key_namespace="my-key-namespace", + wrapping_key=aes_key_bytes, + wrapping_alg=AesWrappingAlg.ALG_AES256_GCM_IV12_TAG16, + ) + + raw_aes_keyring = mat_prov.create_raw_aes_keyring(input=raw_aes_keyring_input) + + # 2. Create the AWS KMS keyring. + # We create a MRK multi keyring, as this interface also supports + # single-region KMS keys (standard KMS keys), + # and creates the KMS client for us automatically. + aws_kms_mrk_multi_keyring = mat_prov.create_aws_kms_mrk_multi_keyring( + input=CreateAwsKmsMrkMultiKeyringInput(generator=key_arn) + ) + + # 3. Create the multi-keyring. + # We will label the AWS KMS keyring as the generator and the raw AES keyring as the + # only child keyring. + # You must provide a generator keyring to encrypt data. + # You may provide additional child keyrings. Each child keyring will be able to + # decrypt data encrypted with the multi-keyring on its own. It does not need + # knowledge of any other child keyrings or the generator keyring to decrypt. + multi_keyring = mat_prov.create_multi_keyring( + input=CreateMultiKeyringInput(generator=aws_kms_mrk_multi_keyring, child_keyrings=[raw_aes_keyring]) + ) + + # 4. 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 + attribute_actions = { + "partition_key": CryptoAction.SIGN_ONLY, # Our partition attribute must be SIGN_ONLY + "sort_key": CryptoAction.SIGN_ONLY, # Our sort attribute must be SIGN_ONLY + "sensitive_data": CryptoAction.ENCRYPT_AND_SIGN, + } + + # 5. Configure which attributes we expect to be included in the signature + # when reading items. There are two options for configuring this: + # + # - (Recommended) Configure `allowed_unsigned_attribute_prefix`: + # 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 `attribute_actions` + # 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 `allowed_unsigned_attributes`: 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 currently authenticate all attributes. To make it easier to + # add unauthenticated attributes in the future, we define a prefix ":" for such attributes. + unsign_attr_prefix = ":" + + # 6. Create the DynamoDb Encryption configuration for the table we will be writing to. + # Note that this example creates one config/client combination for PUT, and another + # for GET. The PUT config uses the multi-keyring, while the GET config uses the + # raw AES keyring. This is solely done to demonstrate that a keyring included as + # a child of a multi-keyring can be used to decrypt data on its own. + table_config = DynamoDbTableEncryptionConfig( + logical_table_name=ddb_table_name, + partition_key_name="partition_key", + sort_key_name="sort_key", + attribute_actions_on_encrypt=attribute_actions, + keyring=multi_keyring, # Multi-keyring is added here + allowed_unsigned_attribute_prefix=unsign_attr_prefix, + ) + + table_configs = {ddb_table_name: table_config} + tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) + + # 7. Create the EncryptedClient + ddb_table = boto3.resource("dynamodb").Table(ddb_table_name) + encrypted_table = EncryptedTable( + table=ddb_table, + encryption_config=tables_config, + ) + # 8. Put an item into our table using the above table. + # Before the item gets sent to DynamoDb, it will be encrypted + # client-side using the multi-keyring. + # The item will be encrypted with all wrapping keys in the keyring, + # so that it can be decrypted with any one of the keys. + item = { + "partition_key": "multiKeyringItem", + "sort_key": 0, + "sensitive_data": "encrypt and sign me!", + } + + put_response = encrypted_table.put_item(Item=item) + + # Demonstrate that PutItem succeeded + assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # 9. Get the item back from our table using the above table. + # The table will decrypt the item client-side using the AWS KMS + # keyring, and return back the original item. + # Since the generator key is the first available key in the keyring, + # that is the key that will be used to decrypt this item. + key_to_get = {"partition_key": "multiKeyringItem", "sort_key": 0} + + get_response = encrypted_table.get_item(Key=key_to_get) + + # Demonstrate that GetItem succeeded and returned the decrypted item + assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + returned_item = get_response["Item"] + assert returned_item["sensitive_data"] == "encrypt and sign me!" + + # 10. Create a new config and client with only the raw AES keyring to GET the item + # This is the same setup as above, except the config uses the `raw_aes_keyring`. + only_aes_keyring_table_config = DynamoDbTableEncryptionConfig( + logical_table_name=ddb_table_name, + partition_key_name="partition_key", + sort_key_name="sort_key", + attribute_actions_on_encrypt=attribute_actions, + keyring=raw_aes_keyring, # Raw AES keyring is added here + allowed_unsigned_attribute_prefix=unsign_attr_prefix, + ) + + only_aes_keyring_table_configs = {ddb_table_name: only_aes_keyring_table_config} + only_aes_keyring_tables_config = DynamoDbTablesEncryptionConfig( + table_encryption_configs=only_aes_keyring_table_configs + ) + + only_aes_keyring_encrypted_table = EncryptedTable(table=ddb_table, encryption_config=only_aes_keyring_tables_config) + + # 11. Get the item back from our table using the client + # configured with only the raw AES keyring. + # The table will decrypt the item client-side using the raw + # AES keyring, and return back the original item. + only_aes_keyring_get_response = only_aes_keyring_encrypted_table.get_item(Key=key_to_get) + + # Demonstrate that GetItem succeeded and returned the decrypted item + assert only_aes_keyring_get_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + only_aes_keyring_returned_item = only_aes_keyring_get_response["Item"] + assert only_aes_keyring_returned_item["sensitive_data"] == "encrypt and sign me!" diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_aes_keyring_example/__init__.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_aes_keyring_example/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_aes_keyring_example/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_aes_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_aes_keyring_example/encryption_config.py similarity index 64% rename from Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_aes_keyring_example.py rename to Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_aes_keyring_example/encryption_config.py index 2b4993e26..f9b0bef68 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_aes_keyring_example.py +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_aes_keyring_example/encryption_config.py @@ -1,35 +1,18 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 """ -Example demonstrating DynamoDb Encryption using a raw AES Keyring. +Configuration module for raw AES keyring encryption setup. -The raw AES Keyring takes in an AES key and uses that key to protect the data -keys that encrypt and decrypt DynamoDb table items. - -This example takes an `aes_key_bytes` parameter representing a 256-bit AES key. -If run through the script's main method, it will create a new key. In practice, -users should not randomly generate a key, but instead retrieve an existing key -from a secure key management system (e.g. an HSM). - -This example encrypts a test item using the provided AES key and puts the encrypted -item to the provided DynamoDb table. Then, it gets the item from the table and -decrypts it. - -Running this example requires access to the DDB Table whose name is provided in -CLI arguments. 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 (S) +This module provides the common encryption configuration used by both +EncryptedClient and EncryptedTable examples. """ -import boto3 from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig from aws_cryptographic_material_providers.mpl.models import ( AesWrappingAlg, CreateRawAesKeyringInput, ) -from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient from aws_dbesdk_dynamodb.structures.dynamodb import ( DynamoDbTableEncryptionConfig, DynamoDbTablesEncryptionConfig, @@ -39,12 +22,13 @@ ) -def raw_aes_keyring_get_item_put_item(ddb_table_name: str, aes_key_bytes: bytes): +def create_encryption_config(ddb_table_name: str, aes_key_bytes: bytes) -> DynamoDbTablesEncryptionConfig: """ - Demonstrate using a raw AES keyring. + Create the encryption configuration for DynamoDB encryption using raw AES keyring. :param ddb_table_name: The name of the DynamoDB table :param aes_key_bytes: The AES key bytes to use + :return: The DynamoDB tables encryption configuration """ # 1. Create the keyring. # The DynamoDb encryption client uses this to encrypt and decrypt items. @@ -112,34 +96,4 @@ def raw_aes_keyring_get_item_put_item(ddb_table_name: str, aes_key_bytes: bytes) ) table_configs = {ddb_table_name: table_config} - tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) - - # 5. Create the EncryptedClient - ddb_client = boto3.client("dynamodb") - encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config) - - # 6. Put an item into our table using the above client. - # Before the item gets sent to DynamoDb, it will be encrypted - # client-side, according to our configuration. - item = { - "partition_key": {"S": "rawAesKeyringItem"}, - "sort_key": {"N": "0"}, - "sensitive_data": {"S": "encrypt and sign me!"}, - } - - put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item) - - # Demonstrate that PutItem succeeded - assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 - - # 7. Get the item back from our table using the same client. - # The client will decrypt the item client-side, and return - # back the original item. - key_to_get = {"partition_key": {"S": "rawAesKeyringItem"}, "sort_key": {"N": "0"}} - - get_response = encrypted_ddb_client.get_item(TableName=ddb_table_name, Key=key_to_get) - - # Demonstrate that GetItem succeeded and returned the decrypted item - assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200 - returned_item = get_response["Item"] - assert returned_item["sensitive_data"]["S"] == "encrypt and sign me!" + return DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_aes_keyring_example/with_encrypted_client.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_aes_keyring_example/with_encrypted_client.py new file mode 100644 index 000000000..b7fdffcba --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_aes_keyring_example/with_encrypted_client.py @@ -0,0 +1,73 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Example demonstrating DynamoDb Encryption using a Raw AES Keyring with EncryptedClient. + +The raw AES Keyring takes in an AES key and uses that key to protect the data +keys that encrypt and decrypt DynamoDb table items. + +This example takes an `aes_key_bytes` parameter representing a 256-bit AES key. +If run through the script's main method, it will create a new key. In practice, +users should not randomly generate a key, but instead retrieve an existing key +from a secure key management system (e.g. an HSM). + +This example encrypts a test item using the provided AES key and puts the encrypted +item to the provided DynamoDb table. Then, it gets the item from the table and +decrypts it. + +Running this example requires access to the DDB Table whose name is provided in +CLI arguments. 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 (S) +""" + +import boto3 +from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient + +from .encryption_config import create_encryption_config + + +def raw_aes_keyring_client_example(ddb_table_name: str, aes_key_bytes: bytes): + """ + Demonstrate using a raw AES keyring with EncryptedClient. + + :param ddb_table_name: The name of the DynamoDB table + :param aes_key_bytes: The AES key bytes to use + """ + # 1. Create the DynamoDb Encryption configuration for the table we will be writing to. + # See encryption_config.py in this directory for detailed steps on the encryption configuration. + tables_config = create_encryption_config( + ddb_table_name=ddb_table_name, + aes_key_bytes=aes_key_bytes, + ) + + # 2. Create the EncryptedClient + ddb_client = boto3.client("dynamodb") + encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config) + + # 3. Put an item into our table using the above client. + # Before the item gets sent to DynamoDb, it will be encrypted + # client-side, according to our configuration. + item = { + "partition_key": {"S": "rawAesKeyringItem"}, + "sort_key": {"N": "0"}, + "sensitive_data": {"S": "encrypt and sign me!"}, + } + + put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item) + + # Demonstrate that PutItem succeeded + assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # 4. Get the item back from our table using the same client. + # The client will decrypt the item client-side, and return + # back the original item. + key_to_get = {"partition_key": {"S": "rawAesKeyringItem"}, "sort_key": {"N": "0"}} + + get_response = encrypted_ddb_client.get_item(TableName=ddb_table_name, Key=key_to_get) + + # Demonstrate that GetItem succeeded and returned the decrypted item + assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + returned_item = get_response["Item"] + assert returned_item["sensitive_data"]["S"] == "encrypt and sign me!" diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_aes_keyring_example/with_encrypted_table.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_aes_keyring_example/with_encrypted_table.py new file mode 100644 index 000000000..dbac65e54 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_aes_keyring_example/with_encrypted_table.py @@ -0,0 +1,76 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Example demonstrating DynamoDb Encryption using a Raw AES Keyring with EncryptedTable. + +The raw AES Keyring takes in an AES key and uses that key to protect the data +keys that encrypt and decrypt DynamoDb table items. + +This example takes an `aes_key_bytes` parameter representing a 256-bit AES key. +If run through the script's main method, it will create a new key. In practice, +users should not randomly generate a key, but instead retrieve an existing key +from a secure key management system (e.g. an HSM). + +This example encrypts a test item using the provided AES key and puts the encrypted +item to the provided DynamoDb table. Then, it gets the item from the table and +decrypts it. + +Running this example requires access to the DDB Table whose name is provided in +CLI arguments. 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 (S) +""" + +import boto3 +from aws_dbesdk_dynamodb.encrypted.table import EncryptedTable + +from .encryption_config import create_encryption_config + + +def raw_aes_keyring_table_example(ddb_table_name: str, aes_key_bytes: bytes): + """ + Demonstrate using a raw AES keyring with EncryptedTable. + + :param ddb_table_name: The name of the DynamoDB table + :param aes_key_bytes: The AES key bytes to use + """ + # 1. Create the DynamoDb Encryption configuration for the table we will be writing to. + # See encryption_config.py in this directory for detailed steps on the encryption configuration. + tables_config = create_encryption_config( + ddb_table_name=ddb_table_name, + aes_key_bytes=aes_key_bytes, + ) + + # 2. Create the EncryptedTable + ddb_table = boto3.resource("dynamodb").Table(ddb_table_name) + encrypted_table = EncryptedTable( + table=ddb_table, + encryption_config=tables_config, + ) + + # 3. Put an item into our table using the above client. + # Before the item gets sent to DynamoDb, it will be encrypted + # client-side, according to our configuration. + item = { + "partition_key": "rawAesKeyringItem", + "sort_key": 0, + "sensitive_data": "encrypt and sign me!", + } + + put_response = encrypted_table.put_item(Item=item) + + # Demonstrate that PutItem succeeded + assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # 4. Get the item back from our table using the same client. + # The client will decrypt the item client-side, and return + # back the original item. + key_to_get = {"partition_key": "rawAesKeyringItem", "sort_key": 0} + + get_response = encrypted_table.get_item(Key=key_to_get) + + # Demonstrate that GetItem succeeded and returned the decrypted item + assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + returned_item = get_response["Item"] + assert returned_item["sensitive_data"] == "encrypt and sign me!" diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_ecdh_keyring_example/__init__.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_ecdh_keyring_example/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_ecdh_keyring_example/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_ecdh_keyring_example/utility.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_ecdh_keyring_example/utility.py new file mode 100644 index 000000000..cbcc34c1e --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_ecdh_keyring_example/utility.py @@ -0,0 +1,140 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Utility functions for the raw ECDH keyring example. + +This module provides functions for handling ECC keys, including: +- Loading key bytes from PEM files +- Checking if new key pairs should be generated +- Generating new ECC key pairs +""" + +import pathlib + +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import ec + +EXAMPLE_ECC_PRIVATE_KEY_FILENAME_SENDER = "RawEcdhKeyringExamplePrivateKeySender.pem" +EXAMPLE_ECC_PRIVATE_KEY_FILENAME_RECIPIENT = "RawEcdhKeyringExamplePrivateKeyRecipient.pem" +EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT = "RawEcdhKeyringExamplePublicKeyRecipient.pem" + + +def load_private_key(private_key_filename: str) -> bytes: + """ + Load private key bytes from a PEM file. + + :param private_key_filename: The filename containing the private key + :return: The UTF-8 PEM-encoded private key bytes + """ + try: + with open(private_key_filename, "rb") as f: + private_key_utf8_encoded = f.read() + return private_key_utf8_encoded + except IOError as e: + raise OSError("IOError while reading the private key from file") from e + + +def load_public_key_bytes(public_key_filename: str) -> bytes: + """ + Load public key bytes from a PEM file and convert to DER format. + + :param public_key_filename: The filename containing the public key + :return: The DER-encoded public key bytes + """ + try: + with open(public_key_filename, "rb") as f: + public_key_utf8_encoded = f.read() + public_key = serialization.load_pem_public_key(public_key_utf8_encoded) + return public_key.public_bytes( + encoding=serialization.Encoding.DER, format=serialization.PublicFormat.SubjectPublicKeyInfo + ) + except IOError as e: + raise OSError("IOError while reading the public key from file") from e + + +def should_generate_new_ecc_key_pairs() -> bool: + """ + Check if new ECC key pairs should be generated. + + :return: True if new key pairs should be generated, False otherwise + """ + private_key_file_sender = pathlib.Path(EXAMPLE_ECC_PRIVATE_KEY_FILENAME_SENDER) + private_key_file_recipient = pathlib.Path(EXAMPLE_ECC_PRIVATE_KEY_FILENAME_RECIPIENT) + public_key_file_recipient = pathlib.Path(EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT) + + # If keys already exist: do not overwrite existing keys + return ( + not private_key_file_sender.exists() + and not public_key_file_recipient.exists() + and not private_key_file_recipient.exists() + ) + + +def generate_ecc_key_pairs(): + """ + Generate new ECC key pairs. + + This code will generate new ECC key pairs for example use. + The keys will be written to the files: + - private_sender: EXAMPLE_ECC_PRIVATE_KEY_FILENAME_SENDER + - private_recipient: EXAMPLE_ECC_PRIVATE_KEY_FILENAME_RECIPIENT + - public_recipient: EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT + This example uses cryptography's EllipticCurve to generate the key pairs. + In practice, you should not generate this in your code, and should instead + retrieve this key from a secure key management system (e.g. HSM). + These examples only demonstrate using the P256 curve while the keyring accepts + P256, P384, or P521. + These keys are created here for example purposes only. + """ + private_key_file_sender = pathlib.Path(EXAMPLE_ECC_PRIVATE_KEY_FILENAME_SENDER) + private_key_file_recipient = pathlib.Path(EXAMPLE_ECC_PRIVATE_KEY_FILENAME_RECIPIENT) + public_key_file_recipient = pathlib.Path(EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT) + + if private_key_file_sender.exists() or public_key_file_recipient.exists() or private_key_file_recipient.exists(): + raise FileExistsError("generateEccKeyPairs will not overwrite existing PEM files") + + # Generate sender key pair + sender_private_key = ec.generate_private_key(ec.SECP256R1()) + + # Generate recipient key pair + recipient_private_key = ec.generate_private_key(ec.SECP256R1()) + recipient_public_key = recipient_private_key.public_key() + + # Write private keys + write_private_key(sender_private_key, EXAMPLE_ECC_PRIVATE_KEY_FILENAME_SENDER) + write_private_key(recipient_private_key, EXAMPLE_ECC_PRIVATE_KEY_FILENAME_RECIPIENT) + + # Write public key + write_public_key(recipient_public_key, EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT) + + +def write_private_key(private_key: ec.EllipticCurvePrivateKey, filename: str): + """ + Write a private key to a PEM file. + + :param private_key: The private key to write + :param filename: The filename to write to + """ + pem_data = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ) + + with open(filename, "wb") as f: + f.write(pem_data) + + +def write_public_key(public_key: ec.EllipticCurvePublicKey, filename: str): + """ + Write a public key to a PEM file. + + :param public_key: The public key to write + :param filename: The filename to write to + """ + pem_data = public_key.public_bytes( + encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo + ) + + with open(filename, "wb") as f: + f.write(pem_data) diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_ecdh_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_ecdh_keyring_example/with_encrypted_client.py similarity index 84% rename from Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_ecdh_keyring_example.py rename to Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_ecdh_keyring_example/with_encrypted_client.py index 54f79a46d..10694b386 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_ecdh_keyring_example.py +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_ecdh_keyring_example/with_encrypted_client.py @@ -1,7 +1,7 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 """ -These examples set up DynamoDb Encryption for the AWS SDK client using the raw ECDH Keyring. +These examples set up DynamoDb Encryption for the AWS SDK client using the raw ECDH Keyring with EncrypedClient. This keyring, depending on its KeyAgreement scheme, takes in the sender's ECC private key, and the recipient's ECC Public Key to derive a shared secret. @@ -15,7 +15,6 @@ - Partition key is named "partition_key" with type (S) - Sort key is named "sort_key" with type (S) """ -import pathlib import boto3 from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders @@ -39,14 +38,15 @@ CryptoAction, ) from cryptography.hazmat.primitives import serialization -from cryptography.hazmat.primitives.asymmetric import ec -EXAMPLE_ECC_PRIVATE_KEY_FILENAME_SENDER = "RawEcdhKeyringExamplePrivateKeySender.pem" -EXAMPLE_ECC_PRIVATE_KEY_FILENAME_RECIPIENT = "RawEcdhKeyringExamplePrivateKeyRecipient.pem" -EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT = "RawEcdhKeyringExamplePublicKeyRecipient.pem" +from .utility import ( + EXAMPLE_ECC_PRIVATE_KEY_FILENAME_RECIPIENT, + EXAMPLE_ECC_PRIVATE_KEY_FILENAME_SENDER, + EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT, +) -def raw_ecdh_keyring_get_item_put_item(ddb_table_name: str, curve_spec: str): +def raw_ecdh_keyring_get_item_put_item_with_ecnrypted_client(ddb_table_name: str, curve_spec: str): """ Demonstrate using a raw ECDH keyring with static keys. @@ -119,10 +119,10 @@ def raw_ecdh_keyring_get_item_put_item(ddb_table_name: str, curve_spec: str): raw_ecdh_keyring = mat_prov.create_raw_ecdh_keyring(input=keyring_input) - put_get_example_with_keyring(raw_ecdh_keyring, ddb_table_name) + put_get_example_with_keyring_with_encrypted_client(raw_ecdh_keyring, ddb_table_name) -def ephemeral_raw_ecdh_keyring_put_item(ddb_table_name: str, curve_spec: str): +def ephemeral_raw_ecdh_keyring_put_item_with_ecnrypted_client(ddb_table_name: str, curve_spec: str): """ Demonstrate using a raw ECDH keyring with ephemeral keys. @@ -172,10 +172,10 @@ def ephemeral_raw_ecdh_keyring_put_item(ddb_table_name: str, curve_spec: str): # A raw ecdh keyring with Ephemeral configuration cannot decrypt data since the key pair # used as the sender is ephemeral. This means that at decrypt time it does not have # the private key that corresponds to the public key that is stored on the message. - put_example_with_keyring(raw_ecdh_keyring, ddb_table_name) + put_example_with_keyring_with_encrypted_client(raw_ecdh_keyring, ddb_table_name) -def discovery_raw_ecdh_keyring_get_item(ddb_table_name: str, curve_spec: str): +def discovery_raw_ecdh_keyring_get_item_with_ecnrypted_client(ddb_table_name: str, curve_spec: str): """ Demonstrate using a raw ECDH keyring with discovery. @@ -220,10 +220,10 @@ def discovery_raw_ecdh_keyring_get_item(ddb_table_name: str, curve_spec: str): # A raw ecdh keyring with discovery configuration cannot encrypt data since the keyring # looks for its configured public key on the message. - get_example_with_keyring(raw_ecdh_keyring, ddb_table_name) + get_example_with_keyring_with_encrypted_client(raw_ecdh_keyring, ddb_table_name) -def put_get_example_with_keyring(raw_ecdh_keyring: IKeyring, ddb_table_name: str): +def put_get_example_with_keyring_with_encrypted_client(raw_ecdh_keyring: IKeyring, ddb_table_name: str): """ Demonstrate put and get operations with a raw ECDH keyring. @@ -316,7 +316,7 @@ def put_get_example_with_keyring(raw_ecdh_keyring: IKeyring, ddb_table_name: str assert returned_item["sensitive_data"]["S"] == "encrypt and sign me!" -def put_example_with_keyring(raw_ecdh_keyring: IKeyring, ddb_table_name: str): +def put_example_with_keyring_with_encrypted_client(raw_ecdh_keyring: IKeyring, ddb_table_name: str): """ Demonstrate put operation with a raw ECDH keyring. @@ -397,7 +397,7 @@ def put_example_with_keyring(raw_ecdh_keyring: IKeyring, ddb_table_name: str): assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 -def get_example_with_keyring(raw_ecdh_keyring: IKeyring, ddb_table_name: str): +def get_example_with_keyring_with_encrypted_client(raw_ecdh_keyring: IKeyring, ddb_table_name: str): """ Demonstrate get operation with a raw ECDH keyring. @@ -474,91 +474,3 @@ def get_example_with_keyring(raw_ecdh_keyring: IKeyring, ddb_table_name: str): assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200 returned_item = get_response["Item"] assert returned_item["sensitive_data"]["S"] == "encrypt and sign me!" - - -def should_generate_new_ecc_key_pairs() -> bool: - """ - Check if new ECC key pairs should be generated. - - :return: True if new key pairs should be generated, False otherwise - """ - private_key_file_sender = pathlib.Path(EXAMPLE_ECC_PRIVATE_KEY_FILENAME_SENDER) - private_key_file_recipient = pathlib.Path(EXAMPLE_ECC_PRIVATE_KEY_FILENAME_RECIPIENT) - public_key_file_recipient = pathlib.Path(EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT) - - # If keys already exist: do not overwrite existing keys - return ( - not private_key_file_sender.exists() - and not public_key_file_recipient.exists() - and not private_key_file_recipient.exists() - ) - - -def generate_ecc_key_pairs(): - """ - Generate new ECC key pairs. - - This code will generate new ECC key pairs for example use. - The keys will be written to the files: - - public_sender: EXAMPLE_ECC_PUBLIC_KEY_FILENAME_SENDER - - private_sender: EXAMPLE_ECC_PRIVATE_KEY_FILENAME_SENDER - - public_recipient: EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT - This example uses cryptography's EllipticCurve to generate the key pairs. - In practice, you should not generate this in your code, and should instead - retrieve this key from a secure key management system (e.g. HSM). - These examples only demonstrate using the P256 curve while the keyring accepts - P256, P384, or P521. - These keys are created here for example purposes only. - """ - private_key_file_sender = pathlib.Path(EXAMPLE_ECC_PRIVATE_KEY_FILENAME_SENDER) - private_key_file_recipient = pathlib.Path(EXAMPLE_ECC_PRIVATE_KEY_FILENAME_RECIPIENT) - public_key_file_recipient = pathlib.Path(EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT) - - if private_key_file_sender.exists() or public_key_file_recipient.exists() or private_key_file_recipient.exists(): - raise FileExistsError("generateEccKeyPairs will not overwrite existing PEM files") - - # Generate sender key pair - sender_private_key = ec.generate_private_key(ec.SECP256R1()) - - # Generate recipient key pair - recipient_private_key = ec.generate_private_key(ec.SECP256R1()) - recipient_public_key = recipient_private_key.public_key() - - # Write private keys - write_private_key(sender_private_key, EXAMPLE_ECC_PRIVATE_KEY_FILENAME_SENDER) - write_private_key(recipient_private_key, EXAMPLE_ECC_PRIVATE_KEY_FILENAME_RECIPIENT) - - # Write public key - write_public_key(recipient_public_key, EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT) - - -def write_private_key(private_key: ec.EllipticCurvePrivateKey, filename: str): - """ - Write a private key to a PEM file. - - :param private_key: The private key to write - :param filename: The filename to write to - """ - pem_data = private_key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.PKCS8, - encryption_algorithm=serialization.NoEncryption(), - ) - - with open(filename, "wb") as f: - f.write(pem_data) - - -def write_public_key(public_key: ec.EllipticCurvePublicKey, filename: str): - """ - Write a public key to a PEM file. - - :param public_key: The public key to write - :param filename: The filename to write to - """ - pem_data = public_key.public_bytes( - encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo - ) - - with open(filename, "wb") as f: - f.write(pem_data) diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_ecdh_keyring_example/with_encrypted_table.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_ecdh_keyring_example/with_encrypted_table.py new file mode 100644 index 000000000..5b13e0fb9 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_ecdh_keyring_example/with_encrypted_table.py @@ -0,0 +1,483 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +These examples set up DynamoDb Encryption for the AWS SDK client using the raw ECDH Keyring with EncrypedTable. + +This keyring, depending on its KeyAgreement scheme, +takes in the sender's ECC private key, and the recipient's ECC Public Key to derive a shared secret. +The keyring uses the shared secret to derive a data key to protect the +data keys that encrypt and decrypt DynamoDb table items. + +Running these examples require access to the DDB Table whose name +is provided in CLI arguments. +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 (S) +""" + +import boto3 +from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders +from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig +from aws_cryptographic_material_providers.mpl.models import ( + CreateRawEcdhKeyringInput, + EphemeralPrivateKeyToStaticPublicKeyInput, + PublicKeyDiscoveryInput, + RawEcdhStaticConfigurationsEphemeralPrivateKeyToStaticPublicKey, + RawEcdhStaticConfigurationsPublicKeyDiscovery, + RawEcdhStaticConfigurationsRawPrivateKeyToStaticPublicKey, + RawPrivateKeyToStaticPublicKeyInput, +) +from aws_cryptographic_material_providers.mpl.references import IKeyring +from aws_dbesdk_dynamodb.encrypted.table import EncryptedTable +from aws_dbesdk_dynamodb.structures.dynamodb import ( + DynamoDbTableEncryptionConfig, + DynamoDbTablesEncryptionConfig, +) +from aws_dbesdk_dynamodb.structures.structured_encryption import ( + CryptoAction, +) +from cryptography.hazmat.primitives import serialization + +from .utility import ( + EXAMPLE_ECC_PRIVATE_KEY_FILENAME_RECIPIENT, + EXAMPLE_ECC_PRIVATE_KEY_FILENAME_SENDER, + EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT, +) + + +def raw_ecdh_keyring_get_item_put_item_with_ecnrypted_table(ddb_table_name: str, curve_spec: str): + """ + Demonstrate using a raw ECDH keyring with static keys. + + This example takes in the sender's private key as a + UTF8 PEM-encoded (PKCS #8 PrivateKeyInfo structures) + located at the file location defined in EXAMPLE_ECC_PRIVATE_KEY_FILENAME_SENDER, + the recipient's public key as a UTF8 PEM-encoded X.509 public key, also known as SubjectPublicKeyInfo (SPKI), + located at the file location defined in EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT, + and the Curve Specification where the keys lie. + + This example encrypts a test item using the provided ECC keys and puts the + encrypted item to the provided DynamoDb table. Then, it gets the + item from the table and decrypts it. + + This examples creates a RawECDH keyring with the RawPrivateKeyToStaticPublicKey key agreement scheme. + For more information on this configuration see: + https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/use-raw-ecdh-keyring.html#raw-ecdh-RawPrivateKeyToStaticPublicKey + + On encrypt, the shared secret is derived from the sender's private key and the recipient's public key. + On decrypt, the shared secret is derived from the sender's private key and the recipient's public key; + however, on decrypt the recipient can construct a keyring such that the shared secret is calculated with + the recipient's private key and the sender's public key. In both scenarios the shared secret will be the same. + + :param ddb_table_name: The name of the DynamoDB table + :param curve_spec: The curve specification to use + """ + # Load key pair from UTF-8 encoded PEM files. + # You may provide your own PEM files to use here. If you provide this, it MUST + # be a key on curve P256. + # If you do not, the main method in this class will generate PEM + # files for example use. Do not use these files for any other purpose. + try: + with open(EXAMPLE_ECC_PRIVATE_KEY_FILENAME_SENDER, "rb") as f: + private_key_utf8_encoded = f.read() + except IOError as e: + raise OSError("IOError while reading the private key from file") from e + + try: + with open(EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT, "rb") as f: + public_key_utf8_encoded = f.read() + public_key = serialization.load_pem_public_key(public_key_utf8_encoded) + public_key_bytes = public_key.public_bytes( + encoding=serialization.Encoding.DER, format=serialization.PublicFormat.SubjectPublicKeyInfo + ) + except IOError as e: + raise OSError("IOError while reading the public key from file") from e + + # Create the keyring. + # This keyring uses static sender and recipient keys. This configuration calls for both of + # the keys to be on the same curve (P256, P384, P521). + # On encrypt, the shared secret is derived from the sender's private key and the recipient's public key. + # For this example, on decrypt, the shared secret is derived from the sender's private key + # and the recipient's public key; + # however, on decrypt the recipient can construct a keyring such that the shared secret is calculated with + # the recipient's private key and the sender's public key. In both scenarios the shared secret will be the same. + # The DynamoDb encryption client uses this to encrypt and decrypt items. + mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig()) + + keyring_input = CreateRawEcdhKeyringInput( + curve_spec=curve_spec, + key_agreement_scheme=RawEcdhStaticConfigurationsRawPrivateKeyToStaticPublicKey( + RawPrivateKeyToStaticPublicKeyInput( + # Must be a UTF8 PEM-encoded private key + sender_static_private_key=private_key_utf8_encoded, + # Must be a DER-encoded X.509 public key + recipient_public_key=public_key_bytes, + ) + ), + ) + + raw_ecdh_keyring = mat_prov.create_raw_ecdh_keyring(input=keyring_input) + + put_get_example_with_keyring_with_encrypted_table(raw_ecdh_keyring, ddb_table_name) + + +def ephemeral_raw_ecdh_keyring_put_item_with_ecnrypted_table(ddb_table_name: str, curve_spec: str): + """ + Demonstrate using a raw ECDH keyring with ephemeral keys. + + This example takes in the recipient's public key located at EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT + as a UTF8 PEM-encoded X.509 public key, and the Curve Specification where the key lies. + + This examples creates a RawECDH keyring with the EphemeralPrivateKeyToStaticPublicKey key agreement scheme. + This configuration will always create a new key pair as the sender key pair for the key agreement operation. + The ephemeral configuration can only encrypt data and CANNOT decrypt messages. + For more information on this configuration see: + https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/use-raw-ecdh-keyring.html#raw-ecdh-EphemeralPrivateKeyToStaticPublicKey + + :param ddb_table_name: The name of the DynamoDB table + :param curve_spec: The curve specification to use + """ + # Load public key from UTF-8 encoded PEM files into a DER encoded public key. + # You may provide your own PEM files to use here. If you provide this, it MUST + # be a key on curve P256. + # If you do not, the main method in this class will generate PEM + # files for example use. Do not use these files for any other purpose. + try: + with open(EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT, "rb") as f: + public_key_utf8_encoded = f.read() + public_key = serialization.load_pem_public_key(public_key_utf8_encoded) + public_key_bytes = public_key.public_bytes( + encoding=serialization.Encoding.DER, format=serialization.PublicFormat.SubjectPublicKeyInfo + ) + except IOError as e: + raise OSError("IOError while reading the public key from file") from e + + # Create the keyring. + # This keyring uses an ephemeral configuration. This configuration will always create a new + # key pair as the sender key pair for the key agreement operation. The ephemeral configuration can only + # encrypt data and CANNOT decrypt messages. + # The DynamoDb encryption client uses this to encrypt items. + mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig()) + + keyring_input = CreateRawEcdhKeyringInput( + curve_spec=curve_spec, + key_agreement_scheme=RawEcdhStaticConfigurationsEphemeralPrivateKeyToStaticPublicKey( + EphemeralPrivateKeyToStaticPublicKeyInput(recipient_public_key=public_key_bytes) + ), + ) + + raw_ecdh_keyring = mat_prov.create_raw_ecdh_keyring(input=keyring_input) + + # A raw ecdh keyring with Ephemeral configuration cannot decrypt data since the key pair + # used as the sender is ephemeral. This means that at decrypt time it does not have + # the private key that corresponds to the public key that is stored on the message. + put_example_with_keyring_with_encrypted_table(raw_ecdh_keyring, ddb_table_name) + + +def discovery_raw_ecdh_keyring_get_item_with_ecnrypted_table(ddb_table_name: str, curve_spec: str): + """ + Demonstrate using a raw ECDH keyring with discovery. + + This example takes in the recipient's private key located at EXAMPLE_ECC_PRIVATE_KEY_FILENAME_RECIPIENT + as a UTF8 PEM-encoded (PKCS #8 PrivateKeyInfo structures) private key, + and the Curve Specification where the key lies. + + This examples creates a RawECDH keyring with the PublicKeyDiscovery key agreement scheme. + This scheme is only available on decrypt. + For more information on this configuration see: + https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/use-raw-ecdh-keyring.html#raw-ecdh-PublicKeyDiscovery + + :param ddb_table_name: The name of the DynamoDB table + :param curve_spec: The curve specification to use + """ + # Load key pair from UTF-8 encoded PEM files. + # You may provide your own PEM files to use here. If you provide this, it MUST + # be a key on curve P256. + # If you do not, the main method in this class will generate PEM + # files for example use. Do not use these files for any other purpose. + try: + with open(EXAMPLE_ECC_PRIVATE_KEY_FILENAME_RECIPIENT, "rb") as f: + private_key_utf8_encoded = f.read() + except IOError as e: + raise OSError("IOError while reading the private key from file") from e + + # Create the keyring. + # This keyring uses a discovery configuration. This configuration will check on decrypt + # if it is meant to decrypt the message by checking if the configured public key is stored on the message. + # The discovery configuration can only decrypt messages and CANNOT encrypt messages. + # The DynamoDb encryption client uses this to decrypt items. + mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig()) + + keyring_input = CreateRawEcdhKeyringInput( + curve_spec=curve_spec, + key_agreement_scheme=RawEcdhStaticConfigurationsPublicKeyDiscovery( + PublicKeyDiscoveryInput(recipient_static_private_key=private_key_utf8_encoded) + ), + ) + + raw_ecdh_keyring = mat_prov.create_raw_ecdh_keyring(input=keyring_input) + + # A raw ecdh keyring with discovery configuration cannot encrypt data since the keyring + # looks for its configured public key on the message. + get_example_with_keyring_with_encrypted_table(raw_ecdh_keyring, ddb_table_name) + + +def put_get_example_with_keyring_with_encrypted_table(raw_ecdh_keyring: IKeyring, ddb_table_name: str): + """ + Demonstrate put and get operations with a raw ECDH keyring. + + :param raw_ecdh_keyring: The raw ECDH keyring to use + :param ddb_table_name: The name of the DynamoDB table + """ + # 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 + attribute_actions = { + "partition_key": CryptoAction.SIGN_ONLY, # Our partition attribute must be SIGN_ONLY + "sort_key": CryptoAction.SIGN_ONLY, # Our sort attribute must be SIGN_ONLY + "sensitive_data": CryptoAction.ENCRYPT_AND_SIGN, + } + + # Configure which attributes we expect to be included in the signature + # when reading items. There are two options for configuring this: + # + # - (Recommended) Configure `allowed_unsigned_attribute_prefix`: + # 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 `attribute_actions` + # 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 `allowed_unsigned_attributes`: 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 currently authenticate all attributes. To make it easier to + # add unauthenticated attributes in the future, we define a prefix ":" for such attributes. + unsign_attr_prefix = ":" + + # Create the DynamoDb Encryption configuration for the table we will be writing to. + table_config = DynamoDbTableEncryptionConfig( + logical_table_name=ddb_table_name, + partition_key_name="partition_key", + sort_key_name="sort_key", + attribute_actions_on_encrypt=attribute_actions, + keyring=raw_ecdh_keyring, + allowed_unsigned_attribute_prefix=unsign_attr_prefix, + ) + + table_configs = {ddb_table_name: table_config} + tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) + + # Create the EncryptedTable + ddb_table = boto3.resource("dynamodb").Table(ddb_table_name) + encrypted_table = EncryptedTable( + table=ddb_table, + encryption_config=tables_config, + ) + + # Put an item into our table using the encrypted table + # Before the item gets sent to DynamoDb, it will be encrypted + # client-side, according to our configuration. + item = { + "partition_key": "rawEcdhKeyringItem", + "sort_key": 0, + "sensitive_data": "encrypt and sign me!", + } + + put_response = encrypted_table.put_item(TableName=ddb_table_name, Item=item) + + # Demonstrate that PutItem succeeded + assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # Get the item back from our table using the encrypted table + # The table will decrypt the item client-side and return the original item. + key_to_get = {"partition_key": "rawEcdhKeyringItem", "sort_key": 0} + + get_response = encrypted_table.get_item(TableName=ddb_table_name, Key=key_to_get) + + # Demonstrate that GetItem succeeded and returned the decrypted item + assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + returned_item = get_response["Item"] + assert returned_item["sensitive_data"] == "encrypt and sign me!" + + +def put_example_with_keyring_with_encrypted_table(raw_ecdh_keyring: IKeyring, ddb_table_name: str): + """ + Demonstrate put operation with a raw ECDH keyring. + + :param raw_ecdh_keyring: The raw ECDH keyring to use + :param ddb_table_name: The name of the DynamoDB table + """ + # 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 + attribute_actions = { + "partition_key": CryptoAction.SIGN_ONLY, # Our partition attribute must be SIGN_ONLY + "sort_key": CryptoAction.SIGN_ONLY, # Our sort attribute must be SIGN_ONLY + "sensitive_data": CryptoAction.ENCRYPT_AND_SIGN, + } + + # Configure which attributes we expect to be included in the signature + # when reading items. There are two options for configuring this: + # + # - (Recommended) Configure `allowed_unsigned_attribute_prefix`: + # 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 `attribute_actions` + # 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 `allowed_unsigned_attributes`: 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 currently authenticate all attributes. To make it easier to + # add unauthenticated attributes in the future, we define a prefix ":" for such attributes. + unsign_attr_prefix = ":" + + # Create the DynamoDb Encryption configuration for the table we will be writing to. + table_config = DynamoDbTableEncryptionConfig( + logical_table_name=ddb_table_name, + partition_key_name="partition_key", + sort_key_name="sort_key", + attribute_actions_on_encrypt=attribute_actions, + keyring=raw_ecdh_keyring, + allowed_unsigned_attribute_prefix=unsign_attr_prefix, + ) + + table_configs = {ddb_table_name: table_config} + tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) + + # Create the EncryptedTable + ddb_table = boto3.resource("dynamodb").Table(ddb_table_name) + encrypted_table = EncryptedTable( + table=ddb_table, + encryption_config=tables_config, + ) + + # Put an item into our table using the encrypted table + # Before the item gets sent to DynamoDb, it will be encrypted + # client-side, according to our configuration. + item = { + "partition_key": "rawEcdhKeyringItem", + "sort_key": 0, + "sensitive_data": "encrypt and sign me!", + } + + put_response = encrypted_table.put_item(TableName=ddb_table_name, Item=item) + + # Demonstrate that PutItem succeeded + assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + +def get_example_with_keyring_with_encrypted_table(raw_ecdh_keyring: IKeyring, ddb_table_name: str): + """ + Demonstrate get operation with a raw ECDH keyring. + + :param raw_ecdh_keyring: The raw ECDH keyring to use + :param ddb_table_name: The name of the DynamoDB table + """ + # 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 + attribute_actions = { + "partition_key": CryptoAction.SIGN_ONLY, # Our partition attribute must be SIGN_ONLY + "sort_key": CryptoAction.SIGN_ONLY, # Our sort attribute must be SIGN_ONLY + "sensitive_data": CryptoAction.ENCRYPT_AND_SIGN, + } + + # Configure which attributes we expect to be included in the signature + # when reading items. There are two options for configuring this: + # + # - (Recommended) Configure `allowed_unsigned_attribute_prefix`: + # 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 `attribute_actions` + # 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 `allowed_unsigned_attributes`: 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 currently authenticate all attributes. To make it easier to + # add unauthenticated attributes in the future, we define a prefix ":" for such attributes. + unsign_attr_prefix = ":" + + # Create the DynamoDb Encryption configuration for the table we will be writing to. + table_config = DynamoDbTableEncryptionConfig( + logical_table_name=ddb_table_name, + partition_key_name="partition_key", + sort_key_name="sort_key", + attribute_actions_on_encrypt=attribute_actions, + keyring=raw_ecdh_keyring, + allowed_unsigned_attribute_prefix=unsign_attr_prefix, + ) + + table_configs = {ddb_table_name: table_config} + tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) + + # Create the EncryptedTable + ddb_table = boto3.resource("dynamodb").Table(ddb_table_name) + encrypted_table = EncryptedTable( + table=ddb_table, + encryption_config=tables_config, + ) + + # Get the item back from our table using the encrypted table + # The table will decrypt the item client-side and return the original item. + key_to_get = {"partition_key": "rawEcdhKeyringItem", "sort_key": 0} + + get_response = encrypted_table.get_item(TableName=ddb_table_name, Key=key_to_get) + + # Demonstrate that GetItem succeeded and returned the decrypted item + assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + returned_item = get_response["Item"] + assert returned_item["sensitive_data"] == "encrypt and sign me!" diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_rsa_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_rsa_keyring_example.py deleted file mode 100644 index f0de43eee..000000000 --- a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_rsa_keyring_example.py +++ /dev/null @@ -1,245 +0,0 @@ -# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: Apache-2.0 -""" -Example demonstrating DynamoDb Encryption using a raw RSA Keyring. - -The raw RSA Keyring uses an RSA key pair to encrypt and decrypt records. -The keyring accepts PEM encodings of the key pair as UTF-8 interpreted bytes. -The client uses the public key to encrypt items it adds to the table and -uses the private key to decrypt existing table items it retrieves. - -The example loads a key pair from PEM files with paths defined in: - - EXAMPLE_RSA_PRIVATE_KEY_FILENAME - - EXAMPLE_RSA_PUBLIC_KEY_FILENAME - -If you do not provide these files, running this example through the main method -will generate these files for you in the directory where the example is run. -In practice, users of this library should not generate new key pairs like this, -and should instead retrieve an existing key from a secure key management system -(e.g. an HSM). - -You may also provide your own key pair by placing PEM files in the directory -where the example is run or modifying the paths in the code below. These files -must be valid PEM encodings of the key pair as UTF-8 encoded bytes. If you do -provide your own key pair, or if a key pair already exists, this class' main -method will not generate a new key pair. - -The example loads a key pair from disk, encrypts a test item, and puts the -encrypted item to the provided DynamoDb table. Then, it gets the item from -the table and decrypts it. - -Running this example requires access to the DDB Table whose name is provided -in CLI arguments. 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 (S) -""" -import os - -import boto3 -from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders -from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig -from aws_cryptographic_material_providers.mpl.models import ( - CreateRawRsaKeyringInput, - PaddingScheme, -) -from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient -from aws_dbesdk_dynamodb.structures.dynamodb import ( - DynamoDbTableEncryptionConfig, - DynamoDbTablesEncryptionConfig, -) -from aws_dbesdk_dynamodb.structures.structured_encryption import ( - CryptoAction, -) -from cryptography.hazmat.primitives import serialization -from cryptography.hazmat.primitives.asymmetric import rsa - -EXAMPLE_RSA_PRIVATE_KEY_FILENAME = "RawRsaKeyringExamplePrivateKey.pem" -EXAMPLE_RSA_PUBLIC_KEY_FILENAME = "RawRsaKeyringExamplePublicKey.pem" - - -def raw_rsa_keyring_example(ddb_table_name: str): - """ - Create a Raw RSA keyring and use it to encrypt/decrypt DynamoDB items. - - :param ddb_table_name: The name of the DynamoDB table - """ - # 1. Load key pair from UTF-8 encoded PEM files. - # You may provide your own PEM files to use here. - # If you do not, the main method in this class will generate PEM - # files for example use. Do not use these files for any other purpose. - try: - with open(EXAMPLE_RSA_PUBLIC_KEY_FILENAME, "rb") as f: - public_key_utf8_encoded = f.read() - except IOError as e: - raise RuntimeError("IOError while reading public key from file") from e - - try: - with open(EXAMPLE_RSA_PRIVATE_KEY_FILENAME, "rb") as f: - private_key_utf8_encoded = f.read() - except IOError as e: - raise RuntimeError("IOError while reading private key from file") from e - - # 2. Create the keyring. - # The DynamoDb encryption client uses this to encrypt and decrypt items. - mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig()) - - keyring_input = CreateRawRsaKeyringInput( - key_name="my-rsa-key-name", - key_namespace="my-key-namespace", - padding_scheme=PaddingScheme.OAEP_SHA256_MGF1, - public_key=public_key_utf8_encoded, - private_key=private_key_utf8_encoded, - ) - - raw_rsa_keyring = mat_prov.create_raw_rsa_keyring(input=keyring_input) - - # 3. 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 - attribute_actions = { - "partition_key": CryptoAction.SIGN_ONLY, # Our partition attribute must be SIGN_ONLY - "sort_key": CryptoAction.SIGN_ONLY, # Our sort attribute must be SIGN_ONLY - "sensitive_data": CryptoAction.ENCRYPT_AND_SIGN, - } - - # 4. Configure which attributes we expect to be included in the signature - # when reading items. There are two options for configuring this: - # - # - (Recommended) Configure `allowed_unsigned_attribute_prefix`: - # 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 `attribute_actions` - # 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 `allowed_unsigned_attributes`: 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 currently authenticate all attributes. To make it easier to - # add unauthenticated attributes in the future, we define a prefix ":" for such attributes. - unsign_attr_prefix = ":" - - # 5. Create the DynamoDb Encryption configuration for the table we will be writing to. - table_config = DynamoDbTableEncryptionConfig( - logical_table_name=ddb_table_name, - partition_key_name="partition_key", - sort_key_name="sort_key", - attribute_actions_on_encrypt=attribute_actions, - keyring=raw_rsa_keyring, - allowed_unsigned_attribute_prefix=unsign_attr_prefix, - ) - - table_configs = {ddb_table_name: table_config} - tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) - - # 6. Create the EncryptedClient - ddb_client = boto3.client("dynamodb") - encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config) - - # 7. Put an item into our table using the above client. - # Before the item gets sent to DynamoDb, it will be encrypted - # client-side using the Raw RSA keyring. - item = { - "partition_key": {"S": "rawRsaKeyringItem"}, - "sort_key": {"N": "0"}, - "sensitive_data": {"S": "encrypt and sign me!"}, - } - - put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item) - - # Demonstrate that PutItem succeeded - assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 - - # 8. Get the item back from our table using the same client. - # The client will decrypt the item client-side using the Raw RSA keyring - # and return the original item. - key_to_get = {"partition_key": {"S": "rawRsaKeyringItem"}, "sort_key": {"N": "0"}} - - get_response = encrypted_ddb_client.get_item(TableName=ddb_table_name, Key=key_to_get) - - # Demonstrate that GetItem succeeded and returned the decrypted item - assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200 - returned_item = get_response["Item"] - assert returned_item["sensitive_data"]["S"] == "encrypt and sign me!" - - -def should_generate_new_rsa_key_pair() -> bool: - """ - Check if we need to generate a new RSA key pair. - - :return: True if we need to generate a new key pair, False otherwise - """ - # Check if a key pair already exists - private_key_file = os.path.exists(EXAMPLE_RSA_PRIVATE_KEY_FILENAME) - public_key_file = os.path.exists(EXAMPLE_RSA_PUBLIC_KEY_FILENAME) - - # If a key pair already exists: do not overwrite existing key pair - if private_key_file and public_key_file: - return False - - # If only one file is present: throw exception - if private_key_file and not public_key_file: - raise ValueError(f"Missing public key file at {EXAMPLE_RSA_PUBLIC_KEY_FILENAME}") - if not private_key_file and public_key_file: - raise ValueError(f"Missing private key file at {EXAMPLE_RSA_PRIVATE_KEY_FILENAME}") - - # If neither file is present, generate a new key pair - return True - - -def generate_rsa_key_pair(): - """Generate a new RSA key pair and save to PEM files.""" - # Safety check: Validate neither file is present - if os.path.exists(EXAMPLE_RSA_PRIVATE_KEY_FILENAME) or os.path.exists(EXAMPLE_RSA_PUBLIC_KEY_FILENAME): - raise FileExistsError("generateRsaKeyPair will not overwrite existing PEM files") - - # This code will generate a new RSA key pair for example use. - # The public and private key will be written to the files: - # - public: EXAMPLE_RSA_PUBLIC_KEY_FILENAME - # - private: EXAMPLE_RSA_PRIVATE_KEY_FILENAME - # In practice, you should not generate this in your code, and should instead - # retrieve this key from a secure key management system (e.g. HSM) - # This key is created here for example purposes only. - private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) - - # Write private key PEM file - private_key_pem = private_key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.PKCS8, - encryption_algorithm=serialization.NoEncryption(), - ) - - try: - with open(EXAMPLE_RSA_PRIVATE_KEY_FILENAME, "wb") as f: - f.write(private_key_pem) - except IOError as e: - raise OSError("IOError while writing private key PEM") from e - - # Write public key PEM file - public_key = private_key.public_key() - public_key_pem = public_key.public_bytes( - encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo - ) - - try: - with open(EXAMPLE_RSA_PUBLIC_KEY_FILENAME, "wb") as f: - f.write(public_key_pem) - except IOError as e: - raise RuntimeError("IOError while writing public key PEM") from e diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_rsa_keyring_example/__init__.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_rsa_keyring_example/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_rsa_keyring_example/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_rsa_keyring_example/encryption_config.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_rsa_keyring_example/encryption_config.py new file mode 100644 index 000000000..62327c50e --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_rsa_keyring_example/encryption_config.py @@ -0,0 +1,103 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Configuration module for raw RSA keyring encryption setup. + +This module provides the common encryption configuration used by both +EncryptedClient and EncryptedTable examples. +""" + +from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders +from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig +from aws_cryptographic_material_providers.mpl.models import ( + CreateRawRsaKeyringInput, + PaddingScheme, +) +from aws_dbesdk_dynamodb.structures.dynamodb import ( + DynamoDbTableEncryptionConfig, + DynamoDbTablesEncryptionConfig, +) +from aws_dbesdk_dynamodb.structures.structured_encryption import ( + CryptoAction, +) + + +def create_encryption_config( + ddb_table_name: str, public_key_utf8_encoded: bytes, private_key_utf8_encoded: bytes +) -> DynamoDbTablesEncryptionConfig: + """ + Create the encryption configuration for DynamoDB encryption using raw RSA keyring. + + :param ddb_table_name: The name of the DynamoDB table + :param public_key_utf8_encoded: The UTF-8 encoded PEM format public key + :param private_key_utf8_encoded: The UTF-8 encoded PEM format private key + :return: The DynamoDB tables encryption configuration + """ + # 1. Create the keyring. + # The DynamoDb encryption client uses this to encrypt and decrypt items. + mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig()) + + keyring_input = CreateRawRsaKeyringInput( + key_name="my-rsa-key-name", + key_namespace="my-key-namespace", + padding_scheme=PaddingScheme.OAEP_SHA256_MGF1, + public_key=public_key_utf8_encoded, + private_key=private_key_utf8_encoded, + ) + + raw_rsa_keyring = mat_prov.create_raw_rsa_keyring(input=keyring_input) + + # 2. 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 + attribute_actions = { + "partition_key": CryptoAction.SIGN_ONLY, # Our partition attribute must be SIGN_ONLY + "sort_key": CryptoAction.SIGN_ONLY, # Our sort attribute must be SIGN_ONLY + "sensitive_data": CryptoAction.ENCRYPT_AND_SIGN, + } + + # 3. Configure which attributes we expect to be included in the signature + # when reading items. There are two options for configuring this: + # + # - (Recommended) Configure `allowed_unsigned_attribute_prefix`: + # 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 `attribute_actions` + # 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 `allowed_unsigned_attributes`: 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 currently authenticate all attributes. To make it easier to + # add unauthenticated attributes in the future, we define a prefix ":" for such attributes. + unsign_attr_prefix = ":" + + # 4. Create the DynamoDb Encryption configuration for the table we will be writing to. + table_config = DynamoDbTableEncryptionConfig( + logical_table_name=ddb_table_name, + partition_key_name="partition_key", + sort_key_name="sort_key", + attribute_actions_on_encrypt=attribute_actions, + keyring=raw_rsa_keyring, + allowed_unsigned_attribute_prefix=unsign_attr_prefix, + ) + + table_configs = {ddb_table_name: table_config} + return DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_rsa_keyring_example/utility.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_rsa_keyring_example/utility.py new file mode 100644 index 000000000..e3914678c --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_rsa_keyring_example/utility.py @@ -0,0 +1,76 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Utility functions for the raw RSA keyring example.""" + +import os + +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import rsa + +EXAMPLE_RSA_PRIVATE_KEY_FILENAME = "RawRsaKeyringExamplePrivateKey.pem" +EXAMPLE_RSA_PUBLIC_KEY_FILENAME = "RawRsaKeyringExamplePublicKey.pem" + + +def should_generate_new_rsa_key_pair() -> bool: + """ + Check if we need to generate a new RSA key pair. + + :return: True if we need to generate a new key pair, False otherwise + """ + # Check if a key pair already exists + private_key_file = os.path.exists(EXAMPLE_RSA_PRIVATE_KEY_FILENAME) + public_key_file = os.path.exists(EXAMPLE_RSA_PUBLIC_KEY_FILENAME) + + # If a key pair already exists: do not overwrite existing key pair + if private_key_file and public_key_file: + return False + + # If only one file is present: throw exception + if private_key_file and not public_key_file: + raise ValueError(f"Missing public key file at {EXAMPLE_RSA_PUBLIC_KEY_FILENAME}") + if not private_key_file and public_key_file: + raise ValueError(f"Missing private key file at {EXAMPLE_RSA_PRIVATE_KEY_FILENAME}") + + # If neither file is present, generate a new key pair + return True + + +def generate_rsa_key_pair(): + """Generate a new RSA key pair and save to PEM files.""" + # Safety check: Validate neither file is present + if os.path.exists(EXAMPLE_RSA_PRIVATE_KEY_FILENAME) or os.path.exists(EXAMPLE_RSA_PUBLIC_KEY_FILENAME): + raise FileExistsError("generateRsaKeyPair will not overwrite existing PEM files") + + # This code will generate a new RSA key pair for example use. + # The public and private key will be written to the files: + # - public: EXAMPLE_RSA_PUBLIC_KEY_FILENAME + # - private: EXAMPLE_RSA_PRIVATE_KEY_FILENAME + # In practice, you should not generate this in your code, and should instead + # retrieve this key from a secure key management system (e.g. HSM) + # This key is created here for example purposes only. + private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + + # Write private key PEM file + private_key_pem = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ) + + try: + with open(EXAMPLE_RSA_PRIVATE_KEY_FILENAME, "wb") as f: + f.write(private_key_pem) + except IOError as e: + raise OSError("IOError while writing private key PEM") from e + + # Write public key PEM file + public_key = private_key.public_key() + public_key_pem = public_key.public_bytes( + encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo + ) + + try: + with open(EXAMPLE_RSA_PUBLIC_KEY_FILENAME, "wb") as f: + f.write(public_key_pem) + except IOError as e: + raise OSError("IOError while writing public key PEM") from e diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_rsa_keyring_example/with_encrypted_client.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_rsa_keyring_example/with_encrypted_client.py new file mode 100644 index 000000000..92343108c --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_rsa_keyring_example/with_encrypted_client.py @@ -0,0 +1,108 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Example demonstrating DynamoDb Encryption using a Raw RSA Keyring with EncryptedClient. + +The raw RSA Keyring uses an RSA key pair to encrypt and decrypt records. +The keyring accepts PEM encodings of the key pair as UTF-8 interpreted bytes. +The client uses the public key to encrypt items it adds to the table and +uses the private key to decrypt existing table items it retrieves. + +The example loads a key pair from PEM files with paths defined in: + - EXAMPLE_RSA_PRIVATE_KEY_FILENAME + - EXAMPLE_RSA_PUBLIC_KEY_FILENAME + +If you do not provide these files, running this example through the main method +will generate these files for you in the directory where the example is run. +In practice, users of this library should not generate new key pairs like this, +and should instead retrieve an existing key from a secure key management system +(e.g. an HSM). + +You may also provide your own key pair by placing PEM files in the directory +where the example is run or modifying the paths in the code below. These files +must be valid PEM encodings of the key pair as UTF-8 encoded bytes. If you do +provide your own key pair, or if a key pair already exists, this class' main +method will not generate a new key pair. + +The example loads a key pair from disk, encrypts a test item, and puts the +encrypted item to the provided DynamoDb table. Then, it gets the item from +the table and decrypts it. + +Running this example requires access to the DDB Table whose name is provided +in CLI arguments. 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 (S) +""" + +import boto3 +from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient + +from .encryption_config import create_encryption_config +from .utility import ( + EXAMPLE_RSA_PRIVATE_KEY_FILENAME, + EXAMPLE_RSA_PUBLIC_KEY_FILENAME, +) + + +def raw_rsa_keyring_client_example(ddb_table_name: str, private_key_path: str, public_key_path: str): + """ + Demonstrate using a raw RSA keyring with EncryptedClient. + + :param ddb_table_name: The name of the DynamoDB table + :param private_key_path: Path to the private key PEM file + :param public_key_path: Path to the public key PEM file + """ + # 1. Load key pair from UTF-8 encoded PEM files. + # You may provide your own PEM files to use here. + # If you do not, the main method in this class will generate PEM + # files for example use. Do not use these files for any other purpose. + try: + with open(EXAMPLE_RSA_PUBLIC_KEY_FILENAME, "rb") as f: + public_key_utf8_encoded = f.read() + except IOError as e: + raise OSError("IOError while reading public key from file") from e + + try: + with open(EXAMPLE_RSA_PRIVATE_KEY_FILENAME, "rb") as f: + private_key_utf8_encoded = f.read() + except IOError as e: + raise OSError("IOError while reading private key from file") from e + + # 2. Create the DynamoDb Encryption configuration for the table we will be writing to. + # See encryption_config.py in this directory for detailed steps on the encryption configuration. + tables_config = create_encryption_config( + ddb_table_name=ddb_table_name, + public_key_utf8_encoded=public_key_utf8_encoded, + private_key_utf8_encoded=private_key_utf8_encoded, + ) + + # 3. Create the EncryptedClient + ddb_client = boto3.client("dynamodb") + encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config) + + # 4. Put an item into our table using the above client. + # Before the item gets sent to DynamoDb, it will be encrypted + # client-side, according to our configuration. + item = { + "partition_key": {"S": "rawRsaKeyringItem"}, + "sort_key": {"N": "0"}, + "sensitive_data": {"S": "encrypt and sign me!"}, + } + + put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item) + + # Demonstrate that PutItem succeeded + assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # 5. Get the item back from our table using the same client. + # The client will decrypt the item client-side, and return + # back the original item. + key_to_get = {"partition_key": {"S": "rawRsaKeyringItem"}, "sort_key": {"N": "0"}} + + get_response = encrypted_ddb_client.get_item(TableName=ddb_table_name, Key=key_to_get) + + # Demonstrate that GetItem succeeded and returned the decrypted item + assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + returned_item = get_response["Item"] + assert returned_item["sensitive_data"]["S"] == "encrypt and sign me!" diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_rsa_keyring_example/with_encrypted_table.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_rsa_keyring_example/with_encrypted_table.py new file mode 100644 index 000000000..bfee7ee87 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_rsa_keyring_example/with_encrypted_table.py @@ -0,0 +1,111 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Example demonstrating DynamoDb Encryption using a Raw RSA Keyring with EncryptedTable. + +The raw RSA Keyring uses an RSA key pair to encrypt and decrypt records. +The keyring accepts PEM encodings of the key pair as UTF-8 interpreted bytes. +The client uses the public key to encrypt items it adds to the table and +uses the private key to decrypt existing table items it retrieves. + +The example loads a key pair from PEM files with paths defined in: + - EXAMPLE_RSA_PRIVATE_KEY_FILENAME + - EXAMPLE_RSA_PUBLIC_KEY_FILENAME + +If you do not provide these files, running this example through the main method +will generate these files for you in the directory where the example is run. +In practice, users of this library should not generate new key pairs like this, +and should instead retrieve an existing key from a secure key management system +(e.g. an HSM). + +You may also provide your own key pair by placing PEM files in the directory +where the example is run or modifying the paths in the code below. These files +must be valid PEM encodings of the key pair as UTF-8 encoded bytes. If you do +provide your own key pair, or if a key pair already exists, this class' main +method will not generate a new key pair. + +The example loads a key pair from disk, encrypts a test item, and puts the +encrypted item to the provided DynamoDb table. Then, it gets the item from +the table and decrypts it. + +Running this example requires access to the DDB Table whose name is provided +in CLI arguments. 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 (S) +""" + +import boto3 +from aws_dbesdk_dynamodb.encrypted.table import EncryptedTable + +from .encryption_config import create_encryption_config +from .utility import ( + EXAMPLE_RSA_PRIVATE_KEY_FILENAME, + EXAMPLE_RSA_PUBLIC_KEY_FILENAME, +) + + +def raw_rsa_keyring_table_example(ddb_table_name: str, private_key_path: str, public_key_path: str): + """ + Demonstrate using a raw RSA keyring with EncryptedTable. + + :param ddb_table_name: The name of the DynamoDB table + :param private_key_path: Path to the private key PEM file + :param public_key_path: Path to the public key PEM file + """ + # 1. Load key pair from UTF-8 encoded PEM files. + # You may provide your own PEM files to use here. + # If you do not, the main method in this class will generate PEM + # files for example use. Do not use these files for any other purpose. + try: + with open(EXAMPLE_RSA_PUBLIC_KEY_FILENAME, "rb") as f: + public_key_utf8_encoded = f.read() + except IOError as e: + raise OSError("IOError while reading public key from file") from e + + try: + with open(EXAMPLE_RSA_PRIVATE_KEY_FILENAME, "rb") as f: + private_key_utf8_encoded = f.read() + except IOError as e: + raise OSError("IOError while reading private key from file") from e + + # 2. Create the DynamoDb Encryption configuration for the table we will be writing to. + # See encryption_config.py in this directory for detailed steps on the encryption configuration. + tables_config = create_encryption_config( + ddb_table_name=ddb_table_name, + public_key_utf8_encoded=public_key_utf8_encoded, + private_key_utf8_encoded=private_key_utf8_encoded, + ) + + # 3. Create the EncryptedTable + ddb_table = boto3.resource("dynamodb").Table(ddb_table_name) + encrypted_table = EncryptedTable( + table=ddb_table, + encryption_config=tables_config, + ) + + # 4. Put an item into our table using the above client. + # Before the item gets sent to DynamoDb, it will be encrypted + # client-side, according to our configuration. + item = { + "partition_key": "rawRsaKeyringItem", + "sort_key": 0, + "sensitive_data": "encrypt and sign me!", + } + + put_response = encrypted_table.put_item(Item=item) + + # Demonstrate that PutItem succeeded + assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # 5. Get the item back from our table using the same client. + # The client will decrypt the item client-side, and return + # back the original item. + key_to_get = {"partition_key": "rawRsaKeyringItem", "sort_key": 0} + + get_response = encrypted_table.get_item(Key=key_to_get) + + # Demonstrate that GetItem succeeded and returned the decrypted item + assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + returned_item = get_response["Item"] + assert returned_item["sensitive_data"] == "encrypt and sign me!" diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/shared_cache_across_hierarchical_keyrings_example/__init__.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/shared_cache_across_hierarchical_keyrings_example/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/shared_cache_across_hierarchical_keyrings_example/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/shared_cache_across_hierarchical_keyrings_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/shared_cache_across_hierarchical_keyrings_example/with_encrypted_client.py similarity index 100% rename from Examples/runtimes/python/DynamoDBEncryption/src/keyring/shared_cache_across_hierarchical_keyrings_example.py rename to Examples/runtimes/python/DynamoDBEncryption/src/keyring/shared_cache_across_hierarchical_keyrings_example/with_encrypted_client.py diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/shared_cache_across_hierarchical_keyrings_example/with_encrypted_table.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/shared_cache_across_hierarchical_keyrings_example/with_encrypted_table.py new file mode 100644 index 000000000..b253e8eb6 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/shared_cache_across_hierarchical_keyrings_example/with_encrypted_table.py @@ -0,0 +1,351 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Example demonstrates how to use a shared cache across multiple Hierarchical Keyrings in single-threaded environments. + +IMPORTANT: This example and the shared cache functionality should ONLY be used in single-threaded environments. +The AWS Cryptographic Material Providers Library (MPL) for Python does not support multithreading for +components that interact with KMS. For more information about multithreading limitations, see: +https://github.com/aws/aws-cryptographic-material-providers-library/blob/main/AwsCryptographicMaterialProviders/runtimes/python/README.rst + +With this functionality, users only need to maintain one common shared cache across multiple +Hierarchical Keyrings with different Key Stores instances/KMS Clients/KMS Keys in a single-threaded environment. + +There are three important parameters that users need to carefully set while providing the shared cache: + +1. Partition ID - Partition ID is an optional parameter provided to the Hierarchical Keyring input, +which distinguishes Cryptographic Material Providers (i.e: Keyrings) writing to a cache. +- If the Partition ID is set and is the same for two Hierarchical Keyrings (or another Material Provider), + they CAN share the same cache entries in the cache. +- If the Partition ID is set and is different for two Hierarchical Keyrings (or another Material Provider), + they CANNOT share the same cache entries in the cache. +- If the Partition ID is not set by the user, it is initialized as a random 16-byte UUID which makes + it unique for every Hierarchical Keyring, and two Hierarchical Keyrings (or another Material Provider) + CANNOT share the same cache entries in the cache. + +2. Logical Key Store Name - This parameter is set by the user when configuring the Key Store for +the Hierarchical Keyring. This is a logical name for the branch key store. +Suppose you have a physical Key Store (K). You create two instances of K (K1 and K2). Now, you create +two Hierarchical Keyrings (HK1 and HK2) with these Key Store instances (K1 and K2 respectively). +- If you want to share cache entries across these two keyrings, you should set the Logical Key Store Names + for both the Key Store instances (K1 and K2) to be the same. +- If you set the Logical Key Store Names for K1 and K2 to be different, HK1 (which uses Key Store instance K1) + and HK2 (which uses Key Store instance K2) will NOT be able to share cache entries. + +3. Branch Key ID - Choose an effective Branch Key ID Schema + +This is demonstrated in the example below. +Notice that both K1 and K2 are instances of the same physical Key Store (K). +You MUST NEVER have two different physical Key Stores with the same Logical Key Store Name. + +Important Note: If you have two or more Hierarchy Keyrings with: +- Same Partition ID +- Same Logical Key Store Name of the Key Store for the Hierarchical Keyring +- Same Branch Key ID +then they WILL share the cache entries in the Shared Cache. +Please make sure that you set all of Partition ID, Logical Key Store Name and Branch Key ID +to be the same for two Hierarchical Keyrings if and only if you want them to share cache entries. + +This example sets up DynamoDb Encryption for the AWS SDK client using the Hierarchical +Keyring, which establishes a key hierarchy where "branch" keys are persisted in DynamoDb. +These branch keys are used to protect your data keys, and these branch keys are themselves +protected by a root KMS Key. + +This example first creates a shared cache that you can use across multiple Hierarchical Keyrings. +The example then configures a Hierarchical Keyring (HK1 and HK2) with the shared cache, +a Branch Key ID and two instances (K1 and K2) of the same physical Key Store (K) respectively, +i.e. HK1 with K1 and HK2 with K2. The example demonstrates that if you set the same Partition ID +for HK1 and HK2, the two keyrings can share cache entries. +If you set different Partition ID of the Hierarchical Keyrings, or different +Logical Key Store Names of the Key Store instances, then the keyrings will NOT +be able to share cache entries. + +Running this example requires access to the DDB Table whose name +is provided in CLI arguments. +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 (S) + +This example also requires using a KMS Key whose ARN +is provided in CLI arguments. You need the following access +on this key: + - GenerateDataKeyWithoutPlaintext + - Decrypt +""" +from typing import Dict + +import boto3 +from aws_cryptographic_material_providers.keystore import KeyStore +from aws_cryptographic_material_providers.keystore.config import KeyStoreConfig +from aws_cryptographic_material_providers.keystore.models import KMSConfigurationKmsKeyArn +from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders +from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig +from aws_cryptographic_material_providers.mpl.models import ( + CacheTypeDefault, + CacheTypeShared, + CreateAwsKmsHierarchicalKeyringInput, + CreateCryptographicMaterialsCacheInput, + DefaultCache, +) +from aws_cryptographic_material_providers.mpl.references import IKeyring +from aws_dbesdk_dynamodb.encrypted.table import EncryptedTable +from aws_dbesdk_dynamodb.structures.dynamodb import ( + DynamoDbTableEncryptionConfig, + DynamoDbTablesEncryptionConfig, +) +from aws_dbesdk_dynamodb.structures.structured_encryption import ( + CryptoAction, +) + + +def get_ddb_table( + ddb_table_name: str, hierarchical_keyring: IKeyring, attribute_actions_on_encrypt: Dict[str, CryptoAction] +) -> boto3.client: + """ + Get a DynamoDB client configured with encryption using the given keyring. + + :param ddb_table_name: The name of the DynamoDB table + :param hierarchical_keyring: The hierarchical keyring to use + :param attribute_actions_on_encrypt: The attribute actions for encryption + :return: The configured DynamoDB client + """ + # Configure which attributes we expect to be included 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 currently authenticate all attributes. To make it easier to + # add unauthenticated attributes in the future, we define a prefix ":" for such attributes. + unsign_attr_prefix = ":" + + # Create the DynamoDb Encryption configuration for the table we will be writing to. + table_config = DynamoDbTableEncryptionConfig( + logical_table_name=ddb_table_name, + partition_key_name="partition_key", + sort_key_name="sort_key", + attribute_actions_on_encrypt=attribute_actions_on_encrypt, + keyring=hierarchical_keyring, + allowed_unsigned_attribute_prefix=unsign_attr_prefix, + ) + + table_configs = {ddb_table_name: table_config} + tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) + + # Create the EncryptedTable + ddb_table = boto3.resource("dynamodb").Table(ddb_table_name) + encrypted_ddb_table = EncryptedTable(table=ddb_table, encryption_config=tables_config) + + return encrypted_ddb_table + + +def put_get_items(ddb_table: boto3.resource): + """ + Put and get items using the given DynamoDB client. + + :param ddb_table: The DynamoDB client to use + """ + # Put an item into our table using the given ddb client. + # Before the item gets sent to DynamoDb, it will be encrypted + # client-side, according to our configuration. + # This example creates a Hierarchical Keyring for a single BranchKeyId. You can, however, use a + # BranchKeyIdSupplier as per your use-case. See the HierarchicalKeyringsExample.java for more + # information. + item = {"partition_key": "id", "sort_key": 0, "sensitive_data": "encrypt and sign me!"} + + put_response = ddb_table.put_item(Item=item) + + # Demonstrate that PutItem succeeded + assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # Get the item back from our table using the same client. + # The client will decrypt the item client-side, and return + # back the original item. + # This example creates a Hierarchical Keyring for a single BranchKeyId. You can, however, use a + # BranchKeyIdSupplier as per your use-case. See the HierarchicalKeyringsExample.java for more + # information. + key_to_get = {"partition_key": "id", "sort_key": 0} + + get_response = ddb_table.get_item(Key=key_to_get) + + # Demonstrate that GetItem succeeded and returned the decrypted item + assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + returned_item = get_response["Item"] + assert returned_item["sensitive_data"] == "encrypt and sign me!" + + +def shared_cache_across_hierarchical_keyrings_example( + ddb_table_name: str, + branch_key_id: str, + key_store_table_name: str, + logical_key_store_name: str, + partition_id: str, + kms_key_id: str, +): + """ + Create multiple hierarchical keyrings sharing a cache and use them to encrypt/decrypt DynamoDB items. + + :param ddb_table_name: The name of the DynamoDB table + :param branch_key_id: The branch key ID to use + :param key_store_table_name: The name of the KeyStore DynamoDB table + :param logical_key_store_name: The logical name for the KeyStore + :param partition_id: The partition ID for cache sharing + :param kms_key_id: ARN of the KMS key + """ + # 1. Create the CryptographicMaterialsCache (CMC) to share across multiple Hierarchical Keyrings + # using the Material Providers Library in a single-threaded environment. + # IMPORTANT: This shared cache must only be used in single-threaded environments as the + # MPL for Python does not support multithreading for KMS operations. + # This CMC takes in: + # - CacheType + mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig()) + + cache = CacheTypeDefault(DefaultCache(entry_capacity=100)) + + cryptographic_materials_cache_input = CreateCryptographicMaterialsCacheInput(cache=cache) + + shared_cryptographic_materials_cache = mat_prov.create_cryptographic_materials_cache( + input=cryptographic_materials_cache_input + ) + + # 2. Create a CacheType object for the sharedCryptographicMaterialsCache + # Note that the `cache` parameter in the Hierarchical Keyring Input takes a `CacheType` as input + shared_cache = CacheTypeShared( + # This is the `Shared` CacheType that passes an already initialized shared cache + shared_cryptographic_materials_cache + ) + + # Initial KeyStore Setup: This example requires that you have already + # created your KeyStore, and have populated it with a new branch key. + + # 3. Configure your KeyStore resource keystore1. + # This SHOULD be the same configuration that you used + # to initially create and populate your KeyStore. + # Note that key_store_table_name is the physical Key Store, + # and keystore1 is instances of this physical Key Store. + keystore1 = KeyStore( + config=KeyStoreConfig( + ddb_client=boto3.client("dynamodb"), + ddb_table_name=key_store_table_name, + logical_key_store_name=logical_key_store_name, + kms_client=boto3.client("kms"), + kms_configuration=KMSConfigurationKmsKeyArn(kms_key_id), + ) + ) + + # 4. Create the Hierarchical Keyring HK1 with Key Store instance K1, partitionId, + # the shared Cache and the BranchKeyId. + # Note that we are now providing an already initialized shared cache instead of just mentioning + # the cache type and the Hierarchical Keyring initializing a cache at initialization. + + # This example creates a Hierarchical Keyring for a single BranchKeyId. You can, however, use a + # BranchKeyIdSupplier as per your use-case. See the HierarchicalKeyringsExample.java for more + # information. + + # Please make sure that you read the guidance on how to set Partition ID, Logical Key Store Name and + # Branch Key ID at the top of this example before creating Hierarchical Keyrings with a Shared Cache. + # partitionId for this example is a random UUID + keyring_input1 = CreateAwsKmsHierarchicalKeyringInput( + key_store=keystore1, + branch_key_id=branch_key_id, + ttl_seconds=600, # This dictates how often we call back to KMS to authorize use of the branch keys + cache=shared_cache, + partition_id=partition_id, + ) + + hierarchical_keyring1 = mat_prov.create_aws_kms_hierarchical_keyring(input=keyring_input1) + + # 5. 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 + attribute_actions_on_encrypt = { + "partition_key": CryptoAction.SIGN_ONLY, # Our partition attribute must be SIGN_ONLY + "sort_key": CryptoAction.SIGN_ONLY, # Our sort attribute must be SIGN_ONLY + "sensitive_data": CryptoAction.ENCRYPT_AND_SIGN, + } + + # 6. Get the DDB Client for Hierarchical Keyring 1. + ddb_table1 = get_ddb_table(ddb_table_name, hierarchical_keyring1, attribute_actions_on_encrypt) + + # 7. Encrypt Decrypt roundtrip with ddb_table1 + put_get_items(ddb_table1) + + # Through the above encrypt and decrypt roundtrip, the cache will be populated and + # the cache entries can be used by another Hierarchical Keyring with the + # - Same Partition ID + # - Same Logical Key Store Name of the Key Store for the Hierarchical Keyring + # - Same Branch Key ID + + # 8. Configure your KeyStore resource keystore2. + # This SHOULD be the same configuration that you used + # to initially create and populate your physical KeyStore. + # Note that key_store_table_name is the physical Key Store, + # and keystore2 is instances of this physical Key Store. + + # Note that for this example, keystore2 is identical to keystore1. + # You can optionally change configurations like KMS Client or KMS Key ID based + # on your use-case. + # Make sure you have the required permissions to use different configurations. + + # - If you want to share cache entries across two keyrings HK1 and HK2, + # you should set the Logical Key Store Names for both + # Key Store instances (K1 and K2) to be the same. + # - If you set the Logical Key Store Names for K1 and K2 to be different, + # HK1 (which uses Key Store instance K1) and HK2 (which uses Key Store + # instance K2) will NOT be able to share cache entries. + keystore2 = KeyStore( + config=KeyStoreConfig( + ddb_client=boto3.client("dynamodb"), + ddb_table_name=key_store_table_name, + logical_key_store_name=logical_key_store_name, + kms_client=boto3.client("kms"), + kms_configuration=KMSConfigurationKmsKeyArn(kms_key_id), + ) + ) + + # 9. Create the Hierarchical Keyring HK2 with Key Store instance K2, the shared Cache + # and the same partitionId and BranchKeyId used in HK1 because we want to share cache entries + # (and experience cache HITS). + + # Please make sure that you read the guidance on how to set Partition ID, Logical Key Store Name and + # Branch Key ID at the top of this example before creating Hierarchical Keyrings with a Shared Cache. + # partitionId for this example is a random UUID + keyring_input2 = CreateAwsKmsHierarchicalKeyringInput( + key_store=keystore2, + branch_key_id=branch_key_id, + ttl_seconds=600, # This dictates how often we call back to KMS to authorize use of the branch keys + cache=shared_cache, + partition_id=partition_id, + ) + + hierarchical_keyring2 = mat_prov.create_aws_kms_hierarchical_keyring(input=keyring_input2) + + # 10. Get the DDB Client for Hierarchical Keyring 2. + ddb_table2 = get_ddb_table(ddb_table_name, hierarchical_keyring2, attribute_actions_on_encrypt) + + # 11. Encrypt Decrypt roundtrip with ddb_table2 + put_get_items(ddb_table2) diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/scan_error_example/__init__.py b/Examples/runtimes/python/DynamoDBEncryption/src/scan_error_example/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/scan_error_example/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/scan_error_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/scan_error_example/encryption_config.py similarity index 81% rename from Examples/runtimes/python/DynamoDBEncryption/src/scan_error_example.py rename to Examples/runtimes/python/DynamoDBEncryption/src/scan_error_example/encryption_config.py index b844b573a..0fae116d1 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/src/scan_error_example.py +++ b/Examples/runtimes/python/DynamoDBEncryption/src/scan_error_example/encryption_config.py @@ -1,22 +1,14 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 - """ -Example demonstrating error handling for failed decryption during DynamoDB Scan operations. - -Uses the Scan operation to show how to retrieve error messages from the -returned CollectionOfErrors when some of the Scan results do not decrypt successfully. +Configuration module for Scan Error Example. -Running this example requires access to the DDB Table whose name is provided in -CLI arguments. 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) +This module provides the common encryption configuration used by both +EncryptedClient and EncryptedTable examples. """ import sys -import boto3 from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig from aws_cryptographic_material_providers.mpl.errors import CollectionOfErrors @@ -30,7 +22,6 @@ CryptoAction, ) - def print_exception(e: Exception, indent: str = ""): """ Print exception and any nested CollectionOfErrors. @@ -47,13 +38,13 @@ def print_exception(e: Exception, indent: str = ""): for err in e.list(): print_exception(err, indent + " ") - -def scan_error(kms_key_id: str, ddb_table_name: str): +def create_encryption_config(kms_key_id: str, ddb_table_name: str) -> DynamoDbTablesEncryptionConfig: """ - Demonstrate handling scan errors. + Create the encryption configuration for DynamoDB encryption using raw AES keyring. - :param kms_key_id: The ARN of the KMS key to use :param ddb_table_name: The name of the DynamoDB table + :param aes_key_bytes: The AES key bytes to use + :return: The DynamoDB tables encryption configuration """ # 1. 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. @@ -129,21 +120,4 @@ def scan_error(kms_key_id: str, ddb_table_name: str): ) table_configs = {ddb_table_name: table_config} - tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) - - # 5. Create the EncryptedClient - ddb_client = boto3.client("dynamodb") - encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config) - - # 6. Perform a Scan for which some records will not decrypt - expression_attribute_values = {":prefix": {"S": "Broken"}} - - try: - encrypted_ddb_client.scan( - TableName=ddb_table_name, - FilterExpression="begins_with(partition_key, :prefix)", - ExpressionAttributeValues=expression_attribute_values, - ) - assert False, "scan should have failed" - except Exception as e: - print_exception(e) + return DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/scan_error_example/with_encrypted_client.py b/Examples/runtimes/python/DynamoDBEncryption/src/scan_error_example/with_encrypted_client.py new file mode 100644 index 000000000..7334ef76d --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/scan_error_example/with_encrypted_client.py @@ -0,0 +1,49 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Example demonstrating error handling for failed decryption during DynamoDB Scan operations with EncryptedClient. + +Uses the Scan operation to show how to retrieve error messages from the +returned CollectionOfErrors when some of the Scan results do not decrypt successfully. + +Running this example requires access to the DDB Table whose name is provided in +CLI arguments. 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) +""" +import boto3 +from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient +from .encryption_config import create_encryption_config, print_exception + +def scan_error_with_client(kms_key_id: str, ddb_table_name: str): + """ + Demonstrate handling scan errors with EncryptedClient. + + :param kms_key_id: The ARN of the KMS key to use + :param ddb_table_name: The name of the DynamoDB table + """ + # 1. Create the DynamoDb Encryption configuration for the table we will be writing to. + # See encryption_config.py in this directory for detailed steps on the encryption configuration. + tables_config = create_encryption_config( + kms_key_id=kms_key_id, + ddb_table_name=ddb_table_name, + ) + + # 2. Create the EncryptedClient + ddb_client = boto3.client("dynamodb") + encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config) + + # 3. Perform a Scan for which some records will not decrypt + expression_attribute_values = {":prefix": {"S": "Broken"}} + + try: + encrypted_ddb_client.scan( + TableName=ddb_table_name, + FilterExpression="begins_with(partition_key, :prefix)", + ExpressionAttributeValues=expression_attribute_values, + ) + assert False, "scan should have failed" + except Exception as e: + print_exception(e) diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/scan_error_example/with_encrypted_table.py b/Examples/runtimes/python/DynamoDBEncryption/src/scan_error_example/with_encrypted_table.py new file mode 100644 index 000000000..816103a19 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/scan_error_example/with_encrypted_table.py @@ -0,0 +1,51 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Example demonstrating error handling for failed decryption during DynamoDB Scan operations with EncryptedClient. + +Uses the Scan operation to show how to retrieve error messages from the +returned CollectionOfErrors when some of the Scan results do not decrypt successfully. + +Running this example requires access to the DDB Table whose name is provided in +CLI arguments. 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) +""" +import boto3 +from aws_dbesdk_dynamodb.encrypted.table import EncryptedTable +from .encryption_config import create_encryption_config, print_exception + +def scan_error_with_table(kms_key_id: str, ddb_table_name: str): + """ + Demonstrate handling scan errors with EncryptedClient. + + :param kms_key_id: The ARN of the KMS key to use + :param ddb_table_name: The name of the DynamoDB table + """ + # 1. Create the DynamoDb Encryption configuration for the table we will be writing to. + # See encryption_config.py in this directory for detailed steps on the encryption configuration. + tables_config = create_encryption_config( + kms_key_id=kms_key_id, + ddb_table_name=ddb_table_name, + ) + + # 2. Create the EncryptedTable + ddb_table = boto3.resource("dynamodb").Table(ddb_table_name) + encrypted_ddb_table = EncryptedTable( + table=ddb_table, + encryption_config=tables_config, + ) + + # 3. Perform a Scan for which some records will not decrypt + expression_attribute_values = {":prefix": "Broken"} + + try: + encrypted_ddb_table.scan( + FilterExpression="begins_with(partition_key, :prefix)", + ExpressionAttributeValues=expression_attribute_values, + ) + assert False, "scan should have failed" + except Exception as e: + print_exception(e) diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/basic_searchable_encryption_example/__init__.py b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/basic_searchable_encryption_example/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/basic_searchable_encryption_example/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/basic_searchable_encryption_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/basic_searchable_encryption_example/beacon_config.py similarity index 65% rename from Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/basic_searchable_encryption_example.py rename to Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/basic_searchable_encryption_example/beacon_config.py index 0b486e872..9f331686a 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/basic_searchable_encryption_example.py +++ b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/basic_searchable_encryption_example/beacon_config.py @@ -1,35 +1,6 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -""" -Example demonstrating DynamoDB encryption using beacons. - -This example demonstrates how to set up a beacon on an encrypted attribute, -put an item with the beacon, and query against that beacon. -This example follows a use case of a database that stores unit inspection information. - -Running this example requires access to a DDB table with the -following key configuration: - - Partition key is named "work_id" with type (S) - - Sort key is named "inspection_date" with type (S) -This table must have a Global Secondary Index (GSI) configured named "last4-unit-index": - - Partition key is named "aws_dbe_b_inspector_id_last4" with type (S) - - Sort key is named "aws_dbe_b_unit" with type (S) - -In this example for storing unit inspection information, this schema is utilized for the data: - - "work_id" stores a unique identifier for a unit inspection work order (v4 UUID) - - "inspection_date" stores an ISO 8601 date for the inspection (YYYY-MM-DD) - - "inspector_id_last4" stores the last 4 digits of the ID of the inspector performing the work - - "unit" stores a 12-digit serial number for the unit being inspected - -The example requires the following ordered input command line parameters: - 1. DDB table name for table to put/query data from - 2. Branch key ID for a branch key that was previously created in your key store. See the - CreateKeyStoreKeyExample. - 3. Branch key wrapping KMS key ARN for the KMS key used to create the branch key with ID - provided in arg 2 - 4. Branch key DDB table name for the DDB table representing the branch key store -""" -import time +"""Sets up the beacon config for basic searchable encryption.""" from typing import List import boto3 @@ -39,7 +10,6 @@ from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig from aws_cryptographic_material_providers.mpl.models import CreateAwsKmsHierarchicalKeyringInput -from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient from aws_dbesdk_dynamodb.structures.dynamodb import ( BeaconKeySourceSingle, BeaconVersion, @@ -54,17 +24,13 @@ GSI_NAME = "last4-unit-index" -def put_item_query_item_with_beacon( - ddb_table_name: str, branch_key_id: str, branch_key_wrapping_kms_key_arn: str, branch_key_ddb_table_name: str +def setup_beacon_config( + ddb_table_name: str, + branch_key_id: str, + branch_key_wrapping_kms_key_arn: str, + branch_key_ddb_table_name: str, ): - """ - Demonstrate using beacons with DynamoDB encryption. - - :param ddb_table_name: The name of the DynamoDB table - :param branch_key_id: Branch key ID for a branch key previously created in key store - :param branch_key_wrapping_kms_key_arn: ARN of KMS key used to create the branch key - :param branch_key_ddb_table_name: Name of DDB table representing the branch key store - """ + """Set up the beacon config for basic searchable encryption.""" # 1. Configure Beacons. # The beacon name must be the name of a table attribute that will be encrypted. # The `length` parameter dictates how many bits are in the beacon attribute value. @@ -237,76 +203,4 @@ def put_item_query_item_with_beacon( ) table_configs = {ddb_table_name: table_config} - tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) - - # 7. Create the EncryptedClient - ddb_client = boto3.client("dynamodb") - encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config) - - # 8. Put an item into our table using the above client. - # Before the item gets sent to DynamoDb, it will be encrypted - # client-side, according to our configuration. - # Since our configuration includes beacons for `inspector_id_last4` and `unit`, - # the client will add two additional attributes to the item. These attributes will have names - # `aws_dbe_b_inspector_id_last4` and `aws_dbe_b_unit`. Their values will be HMACs - # truncated to as many bits as the beacon's `length` parameter; e.g. - # aws_dbe_b_inspector_id_last4 = truncate(HMAC("4321"), 10) - # aws_dbe_b_unit = truncate(HMAC("123456789012"), 30) - item = { - "work_id": {"S": "1313ba89-5661-41eb-ba6c-cb1b4cb67b2d"}, - "inspection_date": {"S": "2023-06-13"}, - "inspector_id_last4": {"S": "4321"}, - "unit": {"S": "123456789012"}, - } - - put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item) - - assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 - - # 9. Query for the item we just put. - # Note that we are constructing the query as if we were querying on plaintext values. - # However, the DDB encryption client will detect that this attribute name has a beacon configured. - # The client will add the beaconized attribute name and attribute value to the query, - # and transform the query to use the beaconized name and value. - # Internally, the client will query for and receive all items with a matching HMAC value in the beacon field. - # This may include a number of "false positives" with different ciphertext, but the same truncated HMAC. - # e.g. if truncate(HMAC("123456789012"), 30) - # == truncate(HMAC("098765432109"), 30), - # the query will return both items. - # The client will decrypt all returned items to determine which ones have the expected attribute values, - # and only surface items with the correct plaintext to the user. - # This procedure is internal to the client and is abstracted away from the user; - # e.g. the user will only see "123456789012" and never - # "098765432109", though the actual query returned both. - expression_attribute_names = {"#last4": "inspector_id_last4", "#unit": "unit"} - - expression_attribute_values = {":last4": {"S": "4321"}, ":unit": {"S": "123456789012"}} - - # GSIs do not update instantly - # so if the results come back empty - # we retry after a short sleep - for _ in range(10): - query_response = encrypted_ddb_client.query( - TableName=ddb_table_name, - IndexName=GSI_NAME, - KeyConditionExpression="#last4 = :last4 and #unit = :unit", - ExpressionAttributeNames=expression_attribute_names, - ExpressionAttributeValues=expression_attribute_values, - ) - - # Validate query was returned successfully - assert query_response["ResponseMetadata"]["HTTPStatusCode"] == 200 - - items = query_response.get("Items", []) - # if no results, sleep and try again - if not items: - time.sleep(0.02) - continue - - # Validate only 1 item was returned: the item we just put - assert len(items) == 1 - returned_item = items[0] - # Validate the item has the expected attributes - assert returned_item["inspector_id_last4"]["S"] == "4321" - assert returned_item["unit"]["S"] == "123456789012" - break + return DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/basic_searchable_encryption_example/with_encrypted_client.py b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/basic_searchable_encryption_example/with_encrypted_client.py new file mode 100644 index 000000000..0dcd7f66c --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/basic_searchable_encryption_example/with_encrypted_client.py @@ -0,0 +1,130 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Example demonstrating DynamoDB encryption using beacons with EncryptedClient. + +This example demonstrates how to set up a beacon on an encrypted attribute, +put an item with the beacon, and query against that beacon. +This example follows a use case of a database that stores unit inspection information. + +Running this example requires access to a DDB table with the +following key configuration: + - Partition key is named "work_id" with type (S) + - Sort key is named "inspection_date" with type (S) +This table must have a Global Secondary Index (GSI) configured named "last4-unit-index": + - Partition key is named "aws_dbe_b_inspector_id_last4" with type (S) + - Sort key is named "aws_dbe_b_unit" with type (S) + +In this example for storing unit inspection information, this schema is utilized for the data: + - "work_id" stores a unique identifier for a unit inspection work order (v4 UUID) + - "inspection_date" stores an ISO 8601 date for the inspection (YYYY-MM-DD) + - "inspector_id_last4" stores the last 4 digits of the ID of the inspector performing the work + - "unit" stores a 12-digit serial number for the unit being inspected + +The example requires the following ordered input command line parameters: + 1. DDB table name for table to put/query data from + 2. Branch key ID for a branch key that was previously created in your key store. See the + CreateKeyStoreKeyExample. + 3. Branch key wrapping KMS key ARN for the KMS key used to create the branch key with ID + provided in arg 2 + 4. Branch key DDB table name for the DDB table representing the branch key store +""" +import time + +import boto3 +from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient + +from DynamoDBEncryption.src.searchable_encryption.basic_searchable_encryption_example.beacon_config import ( + GSI_NAME, + setup_beacon_config, +) + + +def basic_searchable_encryption_client_example( + ddb_table_name: str, branch_key_id: str, branch_key_wrapping_kms_key_arn: str, branch_key_ddb_table_name: str +): + """ + Demonstrate using beacons with DynamoDB encryption with EncryptedClient. + + :param ddb_table_name: The name of the DynamoDB table + :param branch_key_id: Branch key ID for a branch key previously created in key store + :param branch_key_wrapping_kms_key_arn: ARN of KMS key used to create the branch key + :param branch_key_ddb_table_name: Name of DDB table representing the branch key store + """ + # 1. Create the DynamoDb Encryption configuration for the table we will be writing to. + # See beacon_config.py in this directory for detailed steps on the encryption configuration. + tables_config = setup_beacon_config( + ddb_table_name, branch_key_id, branch_key_wrapping_kms_key_arn, branch_key_ddb_table_name + ) + + # 2. Create the EncryptedClient + ddb_client = boto3.client("dynamodb") + encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config) + + # 3. Put an item into our table using the above client. + # Before the item gets sent to DynamoDb, it will be encrypted + # client-side, according to our configuration. + # Since our configuration includes beacons for `inspector_id_last4` and `unit`, + # the client will add two additional attributes to the item. These attributes will have names + # `aws_dbe_b_inspector_id_last4` and `aws_dbe_b_unit`. Their values will be HMACs + # truncated to as many bits as the beacon's `length` parameter; e.g. + # aws_dbe_b_inspector_id_last4 = truncate(HMAC("4321"), 10) + # aws_dbe_b_unit = truncate(HMAC("123456789012"), 30) + item = { + "work_id": {"S": "1313ba89-5661-41eb-ba6c-cb1b4cb67b2d"}, + "inspection_date": {"S": "2023-06-13"}, + "inspector_id_last4": {"S": "4321"}, + "unit": {"S": "123456789012"}, + } + + put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item) + + assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # 4. Query for the item we just put. + # Note that we are constructing the query as if we were querying on plaintext values. + # However, the DDB encryption client will detect that this attribute name has a beacon configured. + # The client will add the beaconized attribute name and attribute value to the query, + # and transform the query to use the beaconized name and value. + # Internally, the client will query for and receive all items with a matching HMAC value in the beacon field. + # This may include a number of "false positives" with different ciphertext, but the same truncated HMAC. + # e.g. if truncate(HMAC("123456789012"), 30) + # == truncate(HMAC("098765432109"), 30), + # the query will return both items. + # The client will decrypt all returned items to determine which ones have the expected attribute values, + # and only surface items with the correct plaintext to the user. + # This procedure is internal to the client and is abstracted away from the user; + # e.g. the user will only see "123456789012" and never + # "098765432109", though the actual query returned both. + expression_attribute_names = {"#last4": "inspector_id_last4", "#unit": "unit"} + + expression_attribute_values = {":last4": {"S": "4321"}, ":unit": {"S": "123456789012"}} + + # GSIs do not update instantly + # so if the results come back empty + # we retry after a short sleep + for _ in range(10): + query_response = encrypted_ddb_client.query( + TableName=ddb_table_name, + IndexName=GSI_NAME, + KeyConditionExpression="#last4 = :last4 and #unit = :unit", + ExpressionAttributeNames=expression_attribute_names, + ExpressionAttributeValues=expression_attribute_values, + ) + + # Validate query was returned successfully + assert query_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + items = query_response.get("Items", []) + # if no results, sleep and try again + if not items: + time.sleep(0.02) + continue + + # Validate only 1 item was returned: the item we just put + assert len(items) == 1 + returned_item = items[0] + # Validate the item has the expected attributes + assert returned_item["inspector_id_last4"]["S"] == "4321" + assert returned_item["unit"]["S"] == "123456789012" + break diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/basic_searchable_encryption_example/with_encrypted_table.py b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/basic_searchable_encryption_example/with_encrypted_table.py new file mode 100644 index 000000000..3c3ddbc29 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/basic_searchable_encryption_example/with_encrypted_table.py @@ -0,0 +1,129 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Example demonstrating DynamoDB encryption using beacons with EncryptedTable. + +This example demonstrates how to set up a beacon on an encrypted attribute, +put an item with the beacon, and query against that beacon. +This example follows a use case of a database that stores unit inspection information. + +Running this example requires access to a DDB table with the +following key configuration: + - Partition key is named "work_id" with type (S) + - Sort key is named "inspection_date" with type (S) +This table must have a Global Secondary Index (GSI) configured named "last4-unit-index": + - Partition key is named "aws_dbe_b_inspector_id_last4" with type (S) + - Sort key is named "aws_dbe_b_unit" with type (S) + +In this example for storing unit inspection information, this schema is utilized for the data: + - "work_id" stores a unique identifier for a unit inspection work order (v4 UUID) + - "inspection_date" stores an ISO 8601 date for the inspection (YYYY-MM-DD) + - "inspector_id_last4" stores the last 4 digits of the ID of the inspector performing the work + - "unit" stores a 12-digit serial number for the unit being inspected + +The example requires the following ordered input command line parameters: + 1. DDB table name for table to put/query data from + 2. Branch key ID for a branch key that was previously created in your key store. See the + CreateKeyStoreKeyExample. + 3. Branch key wrapping KMS key ARN for the KMS key used to create the branch key with ID + provided in arg 2 + 4. Branch key DDB table name for the DDB table representing the branch key store +""" +import time + +import boto3 +from aws_dbesdk_dynamodb.encrypted.table import EncryptedTable + +from .beacon_config import ( + GSI_NAME, + setup_beacon_config, +) + + +def basic_searchable_encryption_table_example( + ddb_table_name: str, branch_key_id: str, branch_key_wrapping_kms_key_arn: str, branch_key_ddb_table_name: str +): + """ + Demonstrate using beacons with DynamoDB encryption with EncryptedTable. + + :param ddb_table_name: The name of the DynamoDB table + :param branch_key_id: Branch key ID for a branch key previously created in key store + :param branch_key_wrapping_kms_key_arn: ARN of KMS key used to create the branch key + :param branch_key_ddb_table_name: Name of DDB table representing the branch key store + """ + # 1. Create the DynamoDb Encryption configuration for the table we will be writing to. + # See beacon_config.py in this directory for detailed steps on the encryption configuration. + tables_config = setup_beacon_config( + ddb_table_name, branch_key_id, branch_key_wrapping_kms_key_arn, branch_key_ddb_table_name + ) + + # 2. Create the EncryptedTable + ddb_table = boto3.resource("dynamodb").Table(ddb_table_name) + encrypted_ddb_table = EncryptedTable(table=ddb_table, encryption_config=tables_config) + + # 3. Put an item into our table using the above encrypted table. + # Before the item gets sent to DynamoDb, it will be encrypted + # client-side, according to our configuration. + # Since our configuration includes beacons for `inspector_id_last4` and `unit`, + # the client will add two additional attributes to the item. These attributes will have names + # `aws_dbe_b_inspector_id_last4` and `aws_dbe_b_unit`. Their values will be HMACs + # truncated to as many bits as the beacon's `length` parameter; e.g. + # aws_dbe_b_inspector_id_last4 = truncate(HMAC("4321"), 10) + # aws_dbe_b_unit = truncate(HMAC("123456789012"), 30) + item = { + "work_id": "1313ba89-5661-41eb-ba6c-cb1b4cb67b2d", + "inspection_date": "2023-06-13", + "inspector_id_last4": "4321", + "unit": "123456789012", + } + + put_response = encrypted_ddb_table.put_item(Item=item) + + assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # 4. Query for the item we just put. + # Note that we are constructing the query as if we were querying on plaintext values. + # However, the DDB encryption client will detect that this attribute name has a beacon configured. + # The client will add the beaconized attribute name and attribute value to the query, + # and transform the query to use the beaconized name and value. + # Internally, the client will query for and receive all items with a matching HMAC value in the beacon field. + # This may include a number of "false positives" with different ciphertext, but the same truncated HMAC. + # e.g. if truncate(HMAC("123456789012"), 30) + # == truncate(HMAC("098765432109"), 30), + # the query will return both items. + # The client will decrypt all returned items to determine which ones have the expected attribute values, + # and only surface items with the correct plaintext to the user. + # This procedure is internal to the client and is abstracted away from the user; + # e.g. the user will only see "123456789012" and never + # "098765432109", though the actual query returned both. + expression_attribute_names = {"#last4": "inspector_id_last4", "#unit": "unit"} + + expression_attribute_values = {":last4": "4321", ":unit": "123456789012"} + + # GSIs do not update instantly + # so if the results come back empty + # we retry after a short sleep + for _ in range(10): + query_response = encrypted_ddb_table.query( + IndexName=GSI_NAME, + KeyConditionExpression="#last4 = :last4 and #unit = :unit", + ExpressionAttributeNames=expression_attribute_names, + ExpressionAttributeValues=expression_attribute_values, + ) + + # Validate query was returned successfully + assert query_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + items = query_response.get("Items", []) + # if no results, sleep and try again + if not items: + time.sleep(0.02) + continue + + # Validate only 1 item was returned: the item we just put + assert len(items) == 1 + returned_item = items[0] + # Validate the item has the expected attributes + assert returned_item["inspector_id_last4"] == "4321" + assert returned_item["unit"] == "123456789012" + break diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/beacon_styles_searchable_encryption_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/beacon_styles_searchable_encryption_example.py deleted file mode 100644 index 06ea52ea6..000000000 --- a/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/beacon_styles_searchable_encryption_example.py +++ /dev/null @@ -1,298 +0,0 @@ -# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: Apache-2.0 -""" -Example demonstrating DynamoDB encryption using beacon styles. - -This example demonstrates how to use Beacons Styles on Standard Beacons on encrypted attributes, - put an item with the beacon, and query against that beacon. -This example follows a use case of a database that stores food information. - This is an extension of the "BasicSearchableEncryptionExample" in this directory - and uses the same table schema. - -Running this example requires access to a DDB table with the -following key configuration: - - Partition key is named "work_id" with type (S) - - Sort key is named "inspection_time" with type (S) - -In this example for storing food information, this schema is utilized for the data: - - "work_id" stores a unique identifier for a unit inspection work order (v4 UUID) - - "inspection_date" stores an ISO 8601 date for the inspection (YYYY-MM-DD) - - "fruit" stores one type of fruit - - "basket" stores a set of types of fruit - - "dessert" stores one type of dessert - - "veggies" stores a set of types of vegetable - - "work_type" stores a unit inspection category - -The example requires the following ordered input command line parameters: - 1. DDB table name for table to put/query data from - 2. Branch key ID for a branch key that was previously created in your key store. See the - CreateKeyStoreKeyExample. - 3. Branch key wrapping KMS key ARN for the KMS key used to create the branch key with ID - provided in arg 2 - 4. Branch key DDB table name for the DDB table representing the branch key store -""" -from typing import List - -import boto3 -from aws_cryptographic_material_providers.keystore.client import KeyStore -from aws_cryptographic_material_providers.keystore.config import KeyStoreConfig -from aws_cryptographic_material_providers.keystore.models import KMSConfigurationKmsKeyArn -from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders -from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig -from aws_cryptographic_material_providers.mpl.models import CreateAwsKmsHierarchicalKeyringInput -from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient -from aws_dbesdk_dynamodb.structures.dynamodb import ( - AsSet, - BeaconKeySourceSingle, - BeaconStyleAsSet, - BeaconStylePartOnly, - BeaconStyleShared, - BeaconStyleSharedSet, - BeaconVersion, - CompoundBeacon, - DynamoDbTableEncryptionConfig, - DynamoDbTablesEncryptionConfig, - EncryptedPart, - PartOnly, - SearchConfig, - Shared, - SharedSet, - SignedPart, - SingleKeyStore, - StandardBeacon, -) -from aws_dbesdk_dynamodb.structures.structured_encryption import CryptoAction - - -def put_item_query_item_with_beacon_styles( - ddb_table_name: str, branch_key_id: str, branch_key_wrapping_kms_key_arn: str, branch_key_ddb_table_name: str -): - """ - Demonstrate using beacon styles with DynamoDB encryption. - - :param ddb_table_name: The name of the DynamoDB table - :param branch_key_id: Branch key ID for a branch key previously created in key store - :param branch_key_wrapping_kms_key_arn: ARN of KMS key used to create the branch key - :param branch_key_ddb_table_name: Name of DDB table representing the branch key store - """ - # 1. Create Beacons. - standard_beacon_list: List[StandardBeacon] = [] - - # The fruit beacon allows searching on the encrypted fruit attribute - # We have selected 30 as an example beacon length, but you should go to - # https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/choosing-beacon-length.html - # when creating your beacons. - fruit_beacon = StandardBeacon(name="fruit", length=30) - standard_beacon_list.append(fruit_beacon) - - # The basket beacon allows searching on the encrypted basket attribute - # basket is used as a Set, and therefore needs a beacon style to reflect that. - # Further, we need to be able to compare the items in basket to the fruit attribute - # so we `share` this beacon with `fruit`. - # Since we need both of these things, we use the SharedSet style. - basket_beacon = StandardBeacon(name="basket", length=30, style=BeaconStyleSharedSet(SharedSet(other="fruit"))) - standard_beacon_list.append(basket_beacon) - - # The dessert beacon allows searching on the encrypted dessert attribute - # We need to be able to compare the dessert attribute to the fruit attribute - # so we `share` this beacon with `fruit`. - dessert_beacon = StandardBeacon(name="dessert", length=30, style=BeaconStyleShared(Shared(other="fruit"))) - standard_beacon_list.append(dessert_beacon) - - # The veggie_beacon allows searching on the encrypted veggies attribute - # veggies is used as a Set, and therefore needs a beacon style to reflect that. - veggie_beacon = StandardBeacon(name="veggies", length=30, style=BeaconStyleAsSet(AsSet())) - standard_beacon_list.append(veggie_beacon) - - # The work_type_beacon allows searching on the encrypted work_type attribute - # We only use it as part of the compound work_unit beacon, - # so we disable its use as a standalone beacon - work_type_beacon = StandardBeacon(name="work_type", length=30, style=BeaconStylePartOnly(PartOnly())) - standard_beacon_list.append(work_type_beacon) - - # Here we build a compound beacon from work_id and work_type - # If we had tried to make a StandardBeacon from work_type, we would have seen an error - # because work_type is "PartOnly" - encrypted_part_list = [EncryptedPart(name="work_type", prefix="T-")] - - signed_part_list = [SignedPart(name="work_id", prefix="I-")] - - compound_beacon_list = [ - CompoundBeacon(name="work_unit", split=".", encrypted=encrypted_part_list, signed=signed_part_list) - ] - - # 2. Configure the Keystore - # These are the same constructions as in the Basic example, which describes these in more detail. - keystore = KeyStore( - config=KeyStoreConfig( - ddb_client=boto3.client("dynamodb"), - ddb_table_name=branch_key_ddb_table_name, - logical_key_store_name=branch_key_ddb_table_name, - kms_client=boto3.client("kms"), - kms_configuration=KMSConfigurationKmsKeyArn(value=branch_key_wrapping_kms_key_arn), - ) - ) - - # 3. Create BeaconVersion. - # This is similar to the Basic example - beacon_versions = [ - BeaconVersion( - standard_beacons=standard_beacon_list, - compound_beacons=compound_beacon_list, - version=1, # MUST be 1 - key_store=keystore, - key_source=BeaconKeySourceSingle(SingleKeyStore(key_id=branch_key_id, cache_ttl=6000)), - ) - ] - - # 4. Create a Hierarchical Keyring - # This is the same configuration as in the Basic example. - mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig()) - - keyring_input = CreateAwsKmsHierarchicalKeyringInput( - branch_key_id=branch_key_id, key_store=keystore, ttl_seconds=6000 - ) - - kms_keyring = mat_prov.create_aws_kms_hierarchical_keyring(input=keyring_input) - - # 5. Configure which attributes are encrypted and/or signed when writing new items. - attribute_actions = { - "work_id": CryptoAction.SIGN_ONLY, # Our partition attribute must be SIGN_ONLY - "inspection_date": CryptoAction.SIGN_ONLY, # Our sort attribute must be SIGN_ONLY - "dessert": CryptoAction.ENCRYPT_AND_SIGN, # Beaconized attributes must be encrypted - "fruit": CryptoAction.ENCRYPT_AND_SIGN, # Beaconized attributes must be encrypted - "basket": CryptoAction.ENCRYPT_AND_SIGN, # Beaconized attributes must be encrypted - "veggies": CryptoAction.ENCRYPT_AND_SIGN, # Beaconized attributes must be encrypted - "work_type": CryptoAction.ENCRYPT_AND_SIGN, # Beaconized attributes must be encrypted - } - - # 6. Create the DynamoDb Encryption configuration for the table we will be writing to. - # The beaconVersions are added to the search configuration. - table_config = DynamoDbTableEncryptionConfig( - logical_table_name=ddb_table_name, - partition_key_name="work_id", - sort_key_name="inspection_date", - attribute_actions_on_encrypt=attribute_actions, - keyring=kms_keyring, - search=SearchConfig(write_version=1, versions=beacon_versions), # MUST be 1 - ) - - table_configs = {ddb_table_name: table_config} - tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) - - # 7. Create the EncryptedClient - ddb_client = boto3.client("dynamodb") - encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config) - - # 8. Create item one, specifically with "dessert != fruit", and "fruit in basket". - item1 = { - "work_id": {"S": "1"}, - "inspection_date": {"S": "2023-06-13"}, - "dessert": {"S": "cake"}, - "fruit": {"S": "banana"}, - "basket": {"SS": ["apple", "banana", "pear"]}, - "veggies": {"SS": ["beans", "carrots", "celery"]}, - "work_type": {"S": "small"}, - } - - # 9. Create item two, specifically with "dessert == fruit", and "fruit not in basket". - item2 = { - "work_id": {"S": "2"}, - "inspection_date": {"S": "2023-06-13"}, - "fruit": {"S": "orange"}, - "dessert": {"S": "orange"}, - "basket": {"SS": ["blackberry", "blueberry", "strawberry"]}, - "veggies": {"SS": ["beans", "carrots", "peas"]}, - "work_type": {"S": "large"}, - } - - # 10. Add the two items - put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item1) - # Validate object put successfully - assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 - - put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item2) - # Validate object put successfully - assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 - - # 11. Test the first type of Set operation: - # Select records where the basket attribute holds a particular value - expression_attribute_values = {":value": {"S": "banana"}} - - scan_response = encrypted_ddb_client.scan( - TableName=ddb_table_name, - FilterExpression="contains(basket, :value)", - ExpressionAttributeValues=expression_attribute_values, - ) - # Validate query was returned successfully - assert scan_response["ResponseMetadata"]["HTTPStatusCode"] == 200 - - # Validate only 1 item was returned: item1 - assert len(scan_response["Items"]) == 1 - assert scan_response["Items"][0] == item1 - - # 12. Test the second type of Set operation: - # Select records where the basket attribute holds the fruit attribute - scan_response = encrypted_ddb_client.scan(TableName=ddb_table_name, FilterExpression="contains(basket, fruit)") - # Validate query was returned successfully - assert scan_response["ResponseMetadata"]["HTTPStatusCode"] == 200 - - # Validate only 1 item was returned: item1 - assert len(scan_response["Items"]) == 1 - assert scan_response["Items"][0] == item1 - - # 13. Test the third type of Set operation: - # Select records where the fruit attribute exists in a particular set - expression_attribute_values = {":value": {"SS": ["boysenberry", "grape", "orange"]}} - - scan_response = encrypted_ddb_client.scan( - TableName=ddb_table_name, - FilterExpression="contains(:value, fruit)", - ExpressionAttributeValues=expression_attribute_values, - ) - # Validate query was returned successfully - assert scan_response["ResponseMetadata"]["HTTPStatusCode"] == 200 - - # Validate only 1 item was returned: item2 - assert len(scan_response["Items"]) == 1 - assert scan_response["Items"][0] == item2 - - # 14. Test a Shared search. Select records where the dessert attribute matches the fruit attribute - scan_response = encrypted_ddb_client.scan(TableName=ddb_table_name, FilterExpression="dessert = fruit") - # Validate query was returned successfully - assert scan_response["ResponseMetadata"]["HTTPStatusCode"] == 200 - - # Validate only 1 item was returned: item2 - assert len(scan_response["Items"]) == 1 - assert scan_response["Items"][0] == item2 - - # 15. Test the AsSet attribute 'veggies': - # Select records where the veggies attribute holds a particular value - expression_attribute_values = {":value": {"S": "peas"}} - - scan_response = encrypted_ddb_client.scan( - TableName=ddb_table_name, - FilterExpression="contains(veggies, :value)", - ExpressionAttributeValues=expression_attribute_values, - ) - # Validate query was returned successfully - assert scan_response["ResponseMetadata"]["HTTPStatusCode"] == 200 - - # Validate only 1 item was returned: item2 - assert len(scan_response["Items"]) == 1 - assert scan_response["Items"][0] == item2 - - # 16. Test the compound beacon 'work_unit': - expression_attribute_values = {":value": {"S": "I-1.T-small"}} - - scan_response = encrypted_ddb_client.scan( - TableName=ddb_table_name, - FilterExpression="work_unit = :value", - ExpressionAttributeValues=expression_attribute_values, - ) - # Validate query was returned successfully - assert scan_response["ResponseMetadata"]["HTTPStatusCode"] == 200 - - # Validate only 1 item was returned: item1 - assert len(scan_response["Items"]) == 1 - assert scan_response["Items"][0] == item1 diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/beacon_styles_searchable_encryption_example/__init__.py b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/beacon_styles_searchable_encryption_example/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/beacon_styles_searchable_encryption_example/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/beacon_styles_searchable_encryption_example/beacon_config.py b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/beacon_styles_searchable_encryption_example/beacon_config.py new file mode 100644 index 000000000..b0136d7c9 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/beacon_styles_searchable_encryption_example/beacon_config.py @@ -0,0 +1,146 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Sets up the beacon config for demonstrating different beacon styles.""" +from typing import List + +import boto3 +from aws_cryptographic_material_providers.keystore.client import KeyStore +from aws_cryptographic_material_providers.keystore.config import KeyStoreConfig +from aws_cryptographic_material_providers.keystore.models import KMSConfigurationKmsKeyArn +from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders +from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig +from aws_cryptographic_material_providers.mpl.models import CreateAwsKmsHierarchicalKeyringInput +from aws_dbesdk_dynamodb.structures.dynamodb import ( + AsSet, + BeaconKeySourceSingle, + BeaconStyleAsSet, + BeaconStylePartOnly, + BeaconStyleShared, + BeaconStyleSharedSet, + BeaconVersion, + CompoundBeacon, + DynamoDbTableEncryptionConfig, + DynamoDbTablesEncryptionConfig, + EncryptedPart, + PartOnly, + SearchConfig, + Shared, + SharedSet, + SignedPart, + SingleKeyStore, + StandardBeacon, +) +from aws_dbesdk_dynamodb.structures.structured_encryption import CryptoAction + + +def setup_beacon_config( + ddb_table_name: str, + branch_key_id: str, + branch_key_wrapping_kms_key_arn: str, + branch_key_ddb_table_name: str, +): + """Set up the beacon config demonstrating different beacon styles.""" + # 1. Create Beacons. + standard_beacon_list: List[StandardBeacon] = [] + + # The fruit beacon allows searching on the encrypted fruit attribute + # We have selected 30 as an example beacon length, but you should go to + # https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/choosing-beacon-length.html + # when creating your beacons. + fruit_beacon = StandardBeacon(name="fruit", length=30) + standard_beacon_list.append(fruit_beacon) + + # The basket beacon allows searching on the encrypted basket attribute + # basket is used as a Set, and therefore needs a beacon style to reflect that. + # Further, we need to be able to compare the items in basket to the fruit attribute + # so we `share` this beacon with `fruit`. + # Since we need both of these things, we use the SharedSet style. + basket_beacon = StandardBeacon(name="basket", length=30, style=BeaconStyleSharedSet(SharedSet(other="fruit"))) + standard_beacon_list.append(basket_beacon) + + # The dessert beacon allows searching on the encrypted dessert attribute + # We need to be able to compare the dessert attribute to the fruit attribute + # so we `share` this beacon with `fruit`. + dessert_beacon = StandardBeacon(name="dessert", length=30, style=BeaconStyleShared(Shared(other="fruit"))) + standard_beacon_list.append(dessert_beacon) + + # The veggie_beacon allows searching on the encrypted veggies attribute + # veggies is used as a Set, and therefore needs a beacon style to reflect that. + veggie_beacon = StandardBeacon(name="veggies", length=30, style=BeaconStyleAsSet(AsSet())) + standard_beacon_list.append(veggie_beacon) + + # The work_type_beacon allows searching on the encrypted work_type attribute + # We only use it as part of the compound work_unit beacon, + # so we disable its use as a standalone beacon + work_type_beacon = StandardBeacon(name="work_type", length=30, style=BeaconStylePartOnly(PartOnly())) + standard_beacon_list.append(work_type_beacon) + + # Here we build a compound beacon from work_id and work_type + # If we had tried to make a StandardBeacon from work_type, we would have seen an error + # because work_type is "PartOnly" + encrypted_part_list = [EncryptedPart(name="work_type", prefix="T-")] + + signed_part_list = [SignedPart(name="work_id", prefix="I-")] + + compound_beacon_list = [ + CompoundBeacon(name="work_unit", split=".", encrypted=encrypted_part_list, signed=signed_part_list) + ] + + # 2. Configure the Keystore + # These are the same constructions as in the Basic example, which describes these in more detail. + keystore = KeyStore( + config=KeyStoreConfig( + ddb_client=boto3.client("dynamodb"), + ddb_table_name=branch_key_ddb_table_name, + logical_key_store_name=branch_key_ddb_table_name, + kms_client=boto3.client("kms"), + kms_configuration=KMSConfigurationKmsKeyArn(value=branch_key_wrapping_kms_key_arn), + ) + ) + + # 3. Create BeaconVersion. + # This is similar to the Basic example + beacon_versions = [ + BeaconVersion( + standard_beacons=standard_beacon_list, + compound_beacons=compound_beacon_list, + version=1, # MUST be 1 + key_store=keystore, + key_source=BeaconKeySourceSingle(SingleKeyStore(key_id=branch_key_id, cache_ttl=6000)), + ) + ] + + # 4. Create a Hierarchical Keyring + # This is the same configuration as in the Basic example. + mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig()) + + keyring_input = CreateAwsKmsHierarchicalKeyringInput( + branch_key_id=branch_key_id, key_store=keystore, ttl_seconds=6000 + ) + + kms_keyring = mat_prov.create_aws_kms_hierarchical_keyring(input=keyring_input) + + # 5. Configure which attributes are encrypted and/or signed when writing new items. + attribute_actions = { + "work_id": CryptoAction.SIGN_ONLY, # Our partition attribute must be SIGN_ONLY + "inspection_date": CryptoAction.SIGN_ONLY, # Our sort attribute must be SIGN_ONLY + "dessert": CryptoAction.ENCRYPT_AND_SIGN, # Beaconized attributes must be encrypted + "fruit": CryptoAction.ENCRYPT_AND_SIGN, # Beaconized attributes must be encrypted + "basket": CryptoAction.ENCRYPT_AND_SIGN, # Beaconized attributes must be encrypted + "veggies": CryptoAction.ENCRYPT_AND_SIGN, # Beaconized attributes must be encrypted + "work_type": CryptoAction.ENCRYPT_AND_SIGN, # Beaconized attributes must be encrypted + } + + # 6. Create the DynamoDb Encryption configuration for the table we will be writing to. + # The beaconVersions are added to the search configuration. + table_config = DynamoDbTableEncryptionConfig( + logical_table_name=ddb_table_name, + partition_key_name="work_id", + sort_key_name="inspection_date", + attribute_actions_on_encrypt=attribute_actions, + keyring=kms_keyring, + search=SearchConfig(write_version=1, versions=beacon_versions), # MUST be 1 + ) + + table_configs = {ddb_table_name: table_config} + return DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/beacon_styles_searchable_encryption_example/with_encrypted_client.py b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/beacon_styles_searchable_encryption_example/with_encrypted_client.py new file mode 100644 index 000000000..17cdfa99b --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/beacon_styles_searchable_encryption_example/with_encrypted_client.py @@ -0,0 +1,175 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Example demonstrating DynamoDB encryption using beacon styles with EncryptedClient. + +This example demonstrates how to use Beacons Styles on Standard Beacons on encrypted attributes, + put an item with the beacon, and query against that beacon. +This example follows a use case of a database that stores food information. + This is an extension of the "BasicSearchableEncryptionExample" in this directory + and uses the same table schema. + +Running this example requires access to a DDB table with the +following key configuration: + - Partition key is named "work_id" with type (S) + - Sort key is named "inspection_time" with type (S) + +In this example for storing food information, this schema is utilized for the data: + - "work_id" stores a unique identifier for a unit inspection work order (v4 UUID) + - "inspection_date" stores an ISO 8601 date for the inspection (YYYY-MM-DD) + - "fruit" stores one type of fruit + - "basket" stores a set of types of fruit + - "dessert" stores one type of dessert + - "veggies" stores a set of types of vegetable + - "work_type" stores a unit inspection category + +The example requires the following ordered input command line parameters: + 1. DDB table name for table to put/query data from + 2. Branch key ID for a branch key that was previously created in your key store. See the + CreateKeyStoreKeyExample. + 3. Branch key wrapping KMS key ARN for the KMS key used to create the branch key with ID + provided in arg 2 + 4. Branch key DDB table name for the DDB table representing the branch key store +""" +import boto3 +from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient + +from .beacon_config import setup_beacon_config + + +def beacon_styles_client_example( + ddb_table_name: str, + branch_key_id: str, + branch_key_wrapping_kms_key_arn: str, + branch_key_ddb_table_name: str, +): + """ + Demonstrate using different beacon styles with EncryptedClient. + + :param ddb_table_name: The name of the DynamoDB table + :param branch_key_id: Branch key ID for a branch key previously created in key store + :param branch_key_wrapping_kms_key_arn: ARN of KMS key used to create the branch key + :param branch_key_ddb_table_name: Name of DDB table representing the branch key store + """ + # 1. Create the DynamoDb Encryption configuration for the table we will be writing to. + # See beacon_config.py in this directory for detailed steps on the encryption configuration. + tables_config = setup_beacon_config( + ddb_table_name, branch_key_id, branch_key_wrapping_kms_key_arn, branch_key_ddb_table_name + ) + + # 2. Create the EncryptedClient + ddb_client = boto3.client("dynamodb") + encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config) + + # 3. Create item one, specifically with "dessert != fruit", and "fruit in basket". + item1 = { + "work_id": {"S": "1"}, + "inspection_date": {"S": "2023-06-13"}, + "dessert": {"S": "cake"}, + "fruit": {"S": "banana"}, + "basket": {"SS": ["apple", "banana", "pear"]}, + "veggies": {"SS": ["beans", "carrots", "celery"]}, + "work_type": {"S": "small"}, + } + + # 4. Create item two, specifically with "dessert == fruit", and "fruit not in basket". + item2 = { + "work_id": {"S": "2"}, + "inspection_date": {"S": "2023-06-13"}, + "fruit": {"S": "orange"}, + "dessert": {"S": "orange"}, + "basket": {"SS": ["blackberry", "blueberry", "strawberry"]}, + "veggies": {"SS": ["beans", "carrots", "peas"]}, + "work_type": {"S": "large"}, + } + + # 5. Add the two items + put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item1) + # Validate object put successfully + assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item2) + # Validate object put successfully + assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # 6. Test the first type of Set operation: + # Select records where the basket attribute holds a particular value + expression_attribute_values = {":value": {"S": "banana"}} + + scan_response = encrypted_ddb_client.scan( + TableName=ddb_table_name, + FilterExpression="contains(basket, :value)", + ExpressionAttributeValues=expression_attribute_values, + ) + # Validate query was returned successfully + assert scan_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # Validate only 1 item was returned: item1 + assert len(scan_response["Items"]) == 1 + assert scan_response["Items"][0] == item1 + + # 7. Test the second type of Set operation: + # Select records where the basket attribute holds the fruit attribute + scan_response = encrypted_ddb_client.scan(TableName=ddb_table_name, FilterExpression="contains(basket, fruit)") + # Validate query was returned successfully + assert scan_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # Validate only 1 item was returned: item1 + assert len(scan_response["Items"]) == 1 + assert scan_response["Items"][0] == item1 + + # 8. Test the third type of Set operation: + # Select records where the fruit attribute exists in a particular set + expression_attribute_values = {":value": {"SS": ["boysenberry", "grape", "orange"]}} + + scan_response = encrypted_ddb_client.scan( + TableName=ddb_table_name, + FilterExpression="contains(:value, fruit)", + ExpressionAttributeValues=expression_attribute_values, + ) + # Validate query was returned successfully + assert scan_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # Validate only 1 item was returned: item2 + assert len(scan_response["Items"]) == 1 + assert scan_response["Items"][0] == item2 + + # 9. Test a Shared search. Select records where the dessert attribute matches the fruit attribute + scan_response = encrypted_ddb_client.scan(TableName=ddb_table_name, FilterExpression="dessert = fruit") + # Validate query was returned successfully + assert scan_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # Validate only 1 item was returned: item2 + assert len(scan_response["Items"]) == 1 + assert scan_response["Items"][0] == item2 + + # 10. Test the AsSet attribute 'veggies': + # Select records where the veggies attribute holds a particular value + expression_attribute_values = {":value": {"S": "peas"}} + + scan_response = encrypted_ddb_client.scan( + TableName=ddb_table_name, + FilterExpression="contains(veggies, :value)", + ExpressionAttributeValues=expression_attribute_values, + ) + # Validate query was returned successfully + assert scan_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # Validate only 1 item was returned: item2 + assert len(scan_response["Items"]) == 1 + assert scan_response["Items"][0] == item2 + + # 11. Test the compound beacon 'work_unit': + expression_attribute_values = {":value": {"S": "I-1.T-small"}} + + scan_response = encrypted_ddb_client.scan( + TableName=ddb_table_name, + FilterExpression="work_unit = :value", + ExpressionAttributeValues=expression_attribute_values, + ) + # Validate query was returned successfully + assert scan_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # Validate only 1 item was returned: item1 + assert len(scan_response["Items"]) == 1 + assert scan_response["Items"][0] == item1 diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/beacon_styles_searchable_encryption_example/with_encrypted_table.py b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/beacon_styles_searchable_encryption_example/with_encrypted_table.py new file mode 100644 index 000000000..63f04f0b8 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/beacon_styles_searchable_encryption_example/with_encrypted_table.py @@ -0,0 +1,174 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Example demonstrating DynamoDB encryption using beacon styles with EncryptedTable. + +This example demonstrates how to use Beacons Styles on Standard Beacons on encrypted attributes, + put an item with the beacon, and query against that beacon. +This example follows a use case of a database that stores food information. + This is an extension of the "BasicSearchableEncryptionExample" in this directory + and uses the same table schema. + +Running this example requires access to a DDB table with the +following key configuration: + - Partition key is named "work_id" with type (S) + - Sort key is named "inspection_time" with type (S) + +In this example for storing food information, this schema is utilized for the data: + - "work_id" stores a unique identifier for a unit inspection work order (v4 UUID) + - "inspection_date" stores an ISO 8601 date for the inspection (YYYY-MM-DD) + - "fruit" stores one type of fruit + - "basket" stores a set of types of fruit + - "dessert" stores one type of dessert + - "veggies" stores a set of types of vegetable + - "work_type" stores a unit inspection category + +The example requires the following ordered input command line parameters: + 1. DDB table name for table to put/query data from + 2. Branch key ID for a branch key that was previously created in your key store. See the + CreateKeyStoreKeyExample. + 3. Branch key wrapping KMS key ARN for the KMS key used to create the branch key with ID + provided in arg 2 + 4. Branch key DDB table name for the DDB table representing the branch key store +""" +import boto3 +from aws_dbesdk_dynamodb.encrypted.table import EncryptedTable + +from .beacon_config import setup_beacon_config + + +def beacon_styles_table_example( + ddb_table_name: str, + branch_key_id: str, + branch_key_wrapping_kms_key_arn: str, + branch_key_ddb_table_name: str, +): + """ + Demonstrate using different beacon styles with EncryptedTable. + + :param ddb_table_name: The name of the DynamoDB table + :param branch_key_id: Branch key ID for a branch key previously created in key store + :param branch_key_wrapping_kms_key_arn: ARN of KMS key used to create the branch key + :param branch_key_ddb_table_name: Name of DDB table representing the branch key store + """ + # 1. Create the DynamoDb Encryption configuration for the table we will be writing to. + # See beacon_config.py in this directory for detailed steps on the encryption configuration. + tables_config = setup_beacon_config( + ddb_table_name, branch_key_id, branch_key_wrapping_kms_key_arn, branch_key_ddb_table_name + ) + + # 2. Create the EncryptedTable + ddb_table = boto3.resource("dynamodb").Table(ddb_table_name) + encrypted_ddb_table = EncryptedTable(table=ddb_table, encryption_config=tables_config) + + # 3. Create item one, specifically with "dessert != fruit", and "fruit in basket". + item1 = { + "work_id": "1", + "inspection_date": "2023-06-13", + "dessert": "cake", + "fruit": "banana", + "basket": {"apple", "banana", "pear"}, + "veggies": {"beans", "carrots", "celery"}, + "work_type": "small", + } + + # 4. Create item two, specifically with "dessert == fruit", and "fruit not in basket". + item2 = { + "work_id": "2", + "inspection_date": "2023-06-13", + "fruit": "orange", + "dessert": "orange", + "basket": {"blackberry", "blueberry", "strawberry"}, + "veggies": {"beans", "carrots", "peas"}, + "work_type": "large", + } + + # 5. Add the two items + put_response = encrypted_ddb_table.put_item(Item=item1) + # Validate object put successfully + assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + put_response = encrypted_ddb_table.put_item(Item=item2) + # Validate object put successfully + assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # 6. Test the first type of Set operation: + # Select records where the basket attribute holds a particular value + expression_attribute_values = {":value": "banana"} + + scan_response = encrypted_ddb_table.scan( + TableName=ddb_table_name, + FilterExpression="contains(basket, :value)", + ExpressionAttributeValues=expression_attribute_values, + ) + # Validate query was returned successfully + assert scan_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # Validate only 1 item was returned: item1 + assert len(scan_response["Items"]) == 1 + assert scan_response["Items"][0] == item1 + + # 7. Test the second type of Set operation: + # Select records where the basket attribute holds the fruit attribute + scan_response = encrypted_ddb_table.scan(FilterExpression="contains(basket, fruit)") + # Validate query was returned successfully + assert scan_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # Validate only 1 item was returned: item1 + assert len(scan_response["Items"]) == 1 + assert scan_response["Items"][0] == item1 + + # 8. Test the third type of Set operation: + # Select records where the fruit attribute exists in a particular set + expression_attribute_values = {":value": {"boysenberry", "grape", "orange"}} + + scan_response = encrypted_ddb_table.scan( + TableName=ddb_table_name, + FilterExpression="contains(:value, fruit)", + ExpressionAttributeValues=expression_attribute_values, + ) + # Validate query was returned successfully + assert scan_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # Validate only 1 item was returned: item2 + assert len(scan_response["Items"]) == 1 + assert scan_response["Items"][0] == item2 + + # 9. Test a Shared search. Select records where the dessert attribute matches the fruit attribute + scan_response = encrypted_ddb_table.scan(FilterExpression="dessert = fruit") + # Validate query was returned successfully + assert scan_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # Validate only 1 item was returned: item2 + assert len(scan_response["Items"]) == 1 + assert scan_response["Items"][0] == item2 + + # 10. Test the AsSet attribute 'veggies': + # Select records where the veggies attribute holds a particular value + expression_attribute_values = {":value": "peas"} + + scan_response = encrypted_ddb_table.scan( + FilterExpression="contains(veggies, :value)", + ExpressionAttributeValues=expression_attribute_values, + ) + # Validate query was returned successfully + assert scan_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # Validate only 1 item was returned: item2 + assert len(scan_response["Items"]) == 1 + assert scan_response["Items"][0] == item2 + + # 11. Test the compound beacon 'work_unit': + expression_attribute_values = {":value": "I-1.T-small"} + + scan_response = encrypted_ddb_table.scan( + TableName=ddb_table_name, + FilterExpression="work_unit = :value", + ExpressionAttributeValues=expression_attribute_values, + ) + # Validate query was returned successfully + assert scan_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # Validate only 1 item was returned: item1 + assert len(scan_response["Items"]) == 1 + assert scan_response["Items"][0] == item1 diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/compound_beacon_searchable_encryption_example/__init__.py b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/compound_beacon_searchable_encryption_example/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/compound_beacon_searchable_encryption_example/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/compound_beacon_searchable_encryption_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/compound_beacon_searchable_encryption_example/beacon_config.py similarity index 50% rename from Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/compound_beacon_searchable_encryption_example.py rename to Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/compound_beacon_searchable_encryption_example/beacon_config.py index 56b10e865..1fa1fafc0 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/compound_beacon_searchable_encryption_example.py +++ b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/compound_beacon_searchable_encryption_example/beacon_config.py @@ -1,43 +1,6 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -""" -Example demonstrating DynamoDB encryption using compound beacons. - -This example demonstrates how to set up a compound beacon on encrypted attributes, - put an item with the beacon, and query against that beacon. -This example follows a use case of a database that stores unit inspection information. - This is an extension of the "BasicSearchableEncryptionExample" in this directory. - This example uses the same situation (storing unit inspection information) - and the same table schema. -However, this example uses a different Global Secondary Index (GSI) - that is based on a compound beacon configuration composed of - the `last4` and `unit` attributes. - -Running this example requires access to a DDB table with the -following key configuration: - - Partition key is named "work_id" with type (S) - - Sort key is named "inspection_time" with type (S) -This table must have a Global Secondary Index (GSI) configured named "last4UnitCompound-index": - - Partition key is named "aws_dbe_b_last4UnitCompound" with type (S) - -In this example for storing unit inspection information, this schema is utilized for the data: - - "work_id" stores a unique identifier for a unit inspection work order (v4 UUID) - - "inspection_date" stores an ISO 8601 date for the inspection (YYYY-MM-DD) - - "inspector_id_last4" stores the last 4 digits of the ID of the inspector performing the work - - "unit" stores a 12-digit serial number for the unit being inspected - -The example requires the following ordered input command line parameters: - 1. DDB table name for table to put/query data from - 2. Branch key ID for a branch key that was previously created in your key store. See the - CreateKeyStoreKeyExample. - 3. Branch key wrapping KMS key ARN for the KMS key used to create the branch key with ID - provided in arg 2 - 4. Branch key DDB table name for the DDB table representing the branch key store -""" -import concurrent.futures -import time -from typing import Dict - +"""Sets up the beacon config for compound beacon searchable encryption.""" import boto3 from aws_cryptographic_material_providers.keystore.client import KeyStore from aws_cryptographic_material_providers.keystore.config import KeyStoreConfig @@ -45,13 +8,6 @@ from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig from aws_cryptographic_material_providers.mpl.models import CreateAwsKmsHierarchicalKeyringInput -from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient -from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_transforms.client import ( - DynamoDbEncryptionTransforms, -) -from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_transforms.models import ( - ResolveAttributesInput, -) from aws_dbesdk_dynamodb.structures.dynamodb import ( BeaconKeySourceSingle, BeaconStylePartOnly, @@ -70,80 +26,15 @@ from aws_dbesdk_dynamodb.structures.structured_encryption import CryptoAction GSI_NAME = "last4UnitCompound-index" -MAX_CONCURRENT_QUERY_THREADS = 10 - - -def put_and_query_item_with_compound_beacon(ddb_client: EncryptedClient, ddb_table_name: str, item: Dict): - """ - Put and query an item using a compound beacon. - - :param ddb_client: The encrypted DynamoDB client - :param ddb_table_name: The name of the DynamoDB table - :param item: The item to put and query - """ - # Write the item to the table - put_response = ddb_client.put_item(TableName=ddb_table_name, Item=item) - # Validate object put successfully - assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 - - # Query for the item we just put. - expression_attribute_names = {"#compound": "last4UnitCompound"} - - # This query expression takes a few factors into consideration: - # - The configured prefix for the last 4 digits of an inspector ID is "L-"; - # the prefix for the unit is "U-" - # - The configured split character, separating component parts, is "." - # - The default constructor adds encrypted parts in the order they are in the encrypted list, which - # configures `last4` to come before `unit`` - # NOTE: We did not need to create a compound beacon for this query. This query could have also been - # done by querying on the partition and sort key, as was done in the Basic example. - # This is intended to be a simple example to demonstrate how one might set up a compound beacon. - # For examples where compound beacons are required, see the Complex example. - # The most basic extension to this example that would require a compound beacon would add a third - # part to the compound beacon, then query against three parts. - expression_attribute_values = {":value": {"S": "L-5678.U-011899988199"}} - - # GSIs do not update instantly - # so if the results come back empty - # we retry after a short sleep - for _ in range(10): - query_response = ddb_client.query( - TableName=ddb_table_name, - IndexName=GSI_NAME, - KeyConditionExpression="#compound = :value", - ExpressionAttributeNames=expression_attribute_names, - ExpressionAttributeValues=expression_attribute_values, - ) - # Validate query was returned successfully - assert query_response["ResponseMetadata"]["HTTPStatusCode"] == 200 - items = query_response.get("Items", []) - # if no results, sleep and try again - if not items: - time.sleep(0.02) - continue - - # Validate only 1 item was returned: the item we just put - assert len(items) == 1 - returned_item = items[0] - # Validate the item has the expected attributes - assert returned_item["inspector_id_last4"]["S"] == "5678" - assert returned_item["unit"]["S"] == "011899988199" - break - - -def put_item_query_item_with_compound_beacon( - ddb_table_name: str, branch_key_id: str, branch_key_wrapping_kms_key_arn: str, branch_key_ddb_table_name: str +def setup_beacon_config( + ddb_table_name: str, + branch_key_id: str, + branch_key_wrapping_kms_key_arn: str, + branch_key_ddb_table_name: str, ): - """ - Demonstrate using compound beacons with DynamoDB encryption. - - :param ddb_table_name: The name of the DynamoDB table - :param branch_key_id: Branch key ID for a branch key previously created in key store - :param branch_key_wrapping_kms_key_arn: ARN of KMS key used to create the branch key - :param branch_key_ddb_table_name: Name of DDB table representing the branch key store - """ + """Set up the beacon config for compound beacon searchable encryption.""" # 1. Create Beacons. # These are the same beacons as in the "BasicSearchableEncryptionExample" in this directory. # See that file to see details on beacon construction and parameters. @@ -265,46 +156,4 @@ def put_item_query_item_with_compound_beacon( ) table_configs = {ddb_table_name: table_config} - tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) - - # 9. Create an item with both attributes used in the compound beacon. - item = { - "work_id": {"S": "9ce39272-8068-4efd-a211-cd162ad65d4c"}, - "inspection_date": {"S": "2023-06-13"}, - "inspector_id_last4": {"S": "5678"}, - "unit": {"S": "011899988199"}, - } - - # 10. If developing or debugging, verify config by checking compound beacon values directly - trans = DynamoDbEncryptionTransforms(config=tables_config) - - resolve_input = ResolveAttributesInput(table_name=ddb_table_name, item=item, version=1) - - resolve_output = trans.resolve_attributes(input=resolve_input) - - # VirtualFields is empty because we have no Virtual Fields configured - assert not resolve_output.virtual_fields - - # Verify that CompoundBeacons has the expected value - cbs = {"last4UnitCompound": "L-5678.U-011899988199"} - assert resolve_output.compound_beacons == cbs - # Note : the compound beacon actually stored in the table is not "L-5678.U-011899988199" - # but rather something like "L-abc.U-123", as both parts are EncryptedParts - # and therefore the text is replaced by the associated beacon - - # 11. Create the EncryptedClient - ddb_client = boto3.client("dynamodb") - encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config) - - put_and_query_item_with_compound_beacon(encrypted_ddb_client, ddb_table_name, item) - - # If instead you were working in a multi-threaded context - # it might look like this - with concurrent.futures.ThreadPoolExecutor(max_workers=MAX_CONCURRENT_QUERY_THREADS) as executor: - futures = [] - for _ in range(2 * MAX_CONCURRENT_QUERY_THREADS): - for _ in range(20): - futures.append( - executor.submit(put_and_query_item_with_compound_beacon, encrypted_ddb_client, ddb_table_name, item) - ) - concurrent.futures.wait(futures, timeout=30) + return DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/compound_beacon_searchable_encryption_example/with_encrypted_client.py b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/compound_beacon_searchable_encryption_example/with_encrypted_client.py new file mode 100644 index 000000000..f611810e5 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/compound_beacon_searchable_encryption_example/with_encrypted_client.py @@ -0,0 +1,164 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Example demonstrating DynamoDB encryption using compound beacons with EncryptedClient. + +This example demonstrates how to set up a compound beacon on encrypted attributes, + put an item with the beacon, and query against that beacon. +This example follows a use case of a database that stores unit inspection information. + This is an extension of the "BasicSearchableEncryptionExample" in this directory. + This example uses the same situation (storing unit inspection information) + and the same table schema. +However, this example uses a different Global Secondary Index (GSI) + that is based on a compound beacon configuration composed of + the `last4` and `unit` attributes. + +Running this example requires access to a DDB table with the +following key configuration: + - Partition key is named "work_id" with type (S) + - Sort key is named "inspection_time" with type (S) +This table must have a Global Secondary Index (GSI) configured named "last4UnitCompound-index": + - Partition key is named "aws_dbe_b_last4UnitCompound" with type (S) + +In this example for storing unit inspection information, this schema is utilized for the data: + - "work_id" stores a unique identifier for a unit inspection work order (v4 UUID) + - "inspection_date" stores an ISO 8601 date for the inspection (YYYY-MM-DD) + - "inspector_id_last4" stores the last 4 digits of the ID of the inspector performing the work + - "unit" stores a 12-digit serial number for the unit being inspected + +The example requires the following ordered input command line parameters: + 1. DDB table name for table to put/query data from + 2. Branch key ID for a branch key that was previously created in your key store. See the + CreateKeyStoreKeyExample. + 3. Branch key wrapping KMS key ARN for the KMS key used to create the branch key with ID + provided in arg 2 + 4. Branch key DDB table name for the DDB table representing the branch key store +""" +import time +from typing import Dict + +import boto3 +from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient +from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_transforms.client import ( + DynamoDbEncryptionTransforms, +) +from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_transforms.models import ( + ResolveAttributesInput, +) + +from DynamoDBEncryption.src.searchable_encryption.compound_beacon_searchable_encryption_example.beacon_config import ( + GSI_NAME, + setup_beacon_config, +) + + +def compound_beacon_client_example( + ddb_table_name: str, + branch_key_id: str, + branch_key_wrapping_kms_key_arn: str, + branch_key_ddb_table_name: str, +): + """ + Demonstrate using compound beacon searchable encryption with EncryptedClient. + + :param ddb_table_name: The name of the DynamoDB table + :param branch_key_id: Branch key ID for a branch key previously created in key store + :param branch_key_wrapping_kms_key_arn: ARN of KMS key used to create the branch key + :param branch_key_ddb_table_name: Name of DDB table representing the branch key store + """ + # 1. Create the DynamoDb Encryption configuration for the table we will be writing to. + # See beacon_config.py in this directory for detailed steps on the encryption configuration. + tables_config = setup_beacon_config( + ddb_table_name, branch_key_id, branch_key_wrapping_kms_key_arn, branch_key_ddb_table_name + ) + + # 2. Create the EncryptedClient + ddb_client = boto3.client("dynamodb") + encrypted_client = EncryptedClient(client=ddb_client, encryption_config=tables_config) + + # 3. Create an item with both attributes used in the compound beacon. + item = { + "work_id": {"S": "9ce39272-8068-4efd-a211-cd162ad65d4c"}, + "inspection_date": {"S": "2023-06-13"}, + "inspector_id_last4": {"S": "5678"}, + "unit": {"S": "011899988199"}, + } + + # 4. If developing or debugging, verify config by checking compound beacon values directly + trans = DynamoDbEncryptionTransforms(config=tables_config) + + resolve_input = ResolveAttributesInput(table_name=ddb_table_name, item=item, version=1) + + resolve_output = trans.resolve_attributes(input=resolve_input) + + # VirtualFields is empty because we have no Virtual Fields configured + assert not resolve_output.virtual_fields + + # Verify that CompoundBeacons has the expected value + cbs = {"last4UnitCompound": "L-5678.U-011899988199"} + assert resolve_output.compound_beacons == cbs + # Note : the compound beacon actually stored in the table is not "L-5678.U-011899988199" + # but rather something like "L-abc.U-123", as both parts are EncryptedParts + # and therefore the text is replaced by the associated beacon + + put_and_query_item_with_compound_beacon(encrypted_client, ddb_table_name, item) + + +def put_and_query_item_with_compound_beacon(ddb_client: EncryptedClient, ddb_table_name: str, item: Dict): + """ + Put and query an item using a compound beacon. + + :param ddb_client: The encrypted DynamoDB client + :param ddb_table_name: The name of the DynamoDB table + :param item: The item to put and query + """ + # Write the item to the table + put_response = ddb_client.put_item(TableName=ddb_table_name, Item=item) + # Validate object put successfully + assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # Query for the item we just put. + expression_attribute_names = {"#compound": "last4UnitCompound"} + + # This query expression takes a few factors into consideration: + # - The configured prefix for the last 4 digits of an inspector ID is "L-"; + # the prefix for the unit is "U-" + # - The configured split character, separating component parts, is "." + # - The default constructor adds encrypted parts in the order they are in the encrypted list, which + # configures `last4` to come before `unit`` + # NOTE: We did not need to create a compound beacon for this query. This query could have also been + # done by querying on the partition and sort key, as was done in the Basic example. + # This is intended to be a simple example to demonstrate how one might set up a compound beacon. + # For examples where compound beacons are required, see the Complex example. + # The most basic extension to this example that would require a compound beacon would add a third + # part to the compound beacon, then query against three parts. + expression_attribute_values = {":value": {"S": "L-5678.U-011899988199"}} + + # GSIs do not update instantly + # so if the results come back empty + # we retry after a short sleep + for _ in range(10): + query_response = ddb_client.query( + TableName=ddb_table_name, + IndexName=GSI_NAME, + KeyConditionExpression="#compound = :value", + ExpressionAttributeNames=expression_attribute_names, + ExpressionAttributeValues=expression_attribute_values, + ) + + # Validate query was returned successfully + assert query_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + items = query_response.get("Items", []) + # if no results, sleep and try again + if not items: + time.sleep(0.02) + continue + + # Validate only 1 item was returned: the item we just put + assert len(items) == 1 + returned_item = items[0] + # Validate the item has the expected attributes + assert returned_item["inspector_id_last4"]["S"] == "5678" + assert returned_item["unit"]["S"] == "011899988199" + break diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/compound_beacon_searchable_encryption_example/with_encrypted_table.py b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/compound_beacon_searchable_encryption_example/with_encrypted_table.py new file mode 100644 index 000000000..2f4797b63 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/compound_beacon_searchable_encryption_example/with_encrypted_table.py @@ -0,0 +1,162 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Example demonstrating DynamoDB encryption using compound beacons with EncryptedTable. + +This example demonstrates how to set up a compound beacon on encrypted attributes, + put an item with the beacon, and query against that beacon. +This example follows a use case of a database that stores unit inspection information. + This is an extension of the "BasicSearchableEncryptionExample" in this directory. + This example uses the same situation (storing unit inspection information) + and the same table schema. +However, this example uses a different Global Secondary Index (GSI) + that is based on a compound beacon configuration composed of + the `last4` and `unit` attributes. + +Running this example requires access to a DDB table with the +following key configuration: + - Partition key is named "work_id" with type (S) + - Sort key is named "inspection_time" with type (S) +This table must have a Global Secondary Index (GSI) configured named "last4UnitCompound-index": + - Partition key is named "aws_dbe_b_last4UnitCompound" with type (S) + +In this example for storing unit inspection information, this schema is utilized for the data: + - "work_id" stores a unique identifier for a unit inspection work order (v4 UUID) + - "inspection_date" stores an ISO 8601 date for the inspection (YYYY-MM-DD) + - "inspector_id_last4" stores the last 4 digits of the ID of the inspector performing the work + - "unit" stores a 12-digit serial number for the unit being inspected + +The example requires the following ordered input command line parameters: + 1. DDB table name for table to put/query data from + 2. Branch key ID for a branch key that was previously created in your key store. See the + CreateKeyStoreKeyExample. + 3. Branch key wrapping KMS key ARN for the KMS key used to create the branch key with ID + provided in arg 2 + 4. Branch key DDB table name for the DDB table representing the branch key store +""" +import time +from typing import Dict + +import boto3 +from aws_dbesdk_dynamodb.encrypted.table import EncryptedTable +from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_transforms.client import ( + DynamoDbEncryptionTransforms, +) +from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_transforms.models import ( + ResolveAttributesInput, +) + +from DynamoDBEncryption.src.searchable_encryption.compound_beacon_searchable_encryption_example.beacon_config import ( + GSI_NAME, + setup_beacon_config, +) + + +def compound_beacon_table_example( + ddb_table_name: str, + branch_key_id: str, + branch_key_wrapping_kms_key_arn: str, + branch_key_ddb_table_name: str, +): + """ + Demonstrate using compound beacon searchable encryption with EncryptedTable. + + :param ddb_table_name: The name of the DynamoDB table + :param branch_key_id: Branch key ID for a branch key previously created in key store + :param branch_key_wrapping_kms_key_arn: ARN of KMS key used to create the branch key + :param branch_key_ddb_table_name: Name of DDB table representing the branch key store + """ + # 1. Create the DynamoDb Encryption configuration for the table we will be writing to. + # See beacon_config.py in this directory for detailed steps on the encryption configuration. + tables_config = setup_beacon_config( + ddb_table_name, branch_key_id, branch_key_wrapping_kms_key_arn, branch_key_ddb_table_name + ) + + # 2. Create the EncryptedTable + ddb_table = boto3.resource("dynamodb").Table(ddb_table_name) + encrypted_table = EncryptedTable(table=ddb_table, encryption_config=tables_config) + + # 3. Create an item with both attributes used in the compound beacon. + item = { + "work_id": "9ce39272-8068-4efd-a211-cd162ad65d4c", + "inspection_date": "2023-06-13", + "inspector_id_last4": "5678", + "unit": "011899988199", + } + + # # 4. If developing or debugging, verify config by checking compound beacon values directly + # trans = DynamoDbEncryptionTransforms(config=tables_config) + # + # resolve_input = ResolveAttributesInput(table_name=ddb_table_name, item=item, version=1) + # + # resolve_output = trans.resolve_attributes(input=resolve_input) + # + # # VirtualFields is empty because we have no Virtual Fields configured + # assert not resolve_output.virtual_fields + # + # # Verify that CompoundBeacons has the expected value + # cbs = {"last4UnitCompound": "L-5678.U-011899988199"} + # assert resolve_output.compound_beacons == cbs + # # Note : the compound beacon actually stored in the table is not "L-5678.U-011899988199" + # # but rather something like "L-abc.U-123", as both parts are EncryptedParts + # # and therefore the text is replaced by the associated beacon + + put_and_query_item_with_compound_beacon(encrypted_table, item) + + +def put_and_query_item_with_compound_beacon(ddb_table: EncryptedTable, item: Dict): + """ + Put and query an item using a compound beacon. + + :param ddb_table: The encrypted DynamoDB client + :param item: The item to put and query + """ + # Write the item to the table + put_response = ddb_table.put_item(Item=item) + # Validate object put successfully + assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # Query for the item we just put. + expression_attribute_names = {"#compound": "last4UnitCompound"} + + # This query expression takes a few factors into consideration: + # - The configured prefix for the last 4 digits of an inspector ID is "L-"; + # the prefix for the unit is "U-" + # - The configured split character, separating component parts, is "." + # - The default constructor adds encrypted parts in the order they are in the encrypted list, which + # configures `last4` to come before `unit`` + # NOTE: We did not need to create a compound beacon for this query. This query could have also been + # done by querying on the partition and sort key, as was done in the Basic example. + # This is intended to be a simple example to demonstrate how one might set up a compound beacon. + # For examples where compound beacons are required, see the Complex example. + # The most basic extension to this example that would require a compound beacon would add a third + # part to the compound beacon, then query against three parts. + expression_attribute_values = {":value": "L-5678.U-011899988199"} + + # GSIs do not update instantly + # so if the results come back empty + # we retry after a short sleep + for _ in range(10): + query_response = ddb_table.query( + IndexName=GSI_NAME, + KeyConditionExpression="#compound = :value", + ExpressionAttributeNames=expression_attribute_names, + ExpressionAttributeValues=expression_attribute_values, + ) + + # Validate query was returned successfully + assert query_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + items = query_response.get("Items", []) + # if no results, sleep and try again + if not items: + time.sleep(0.02) + continue + + # Validate only 1 item was returned: the item we just put + assert len(items) == 1 + returned_item = items[0] + # Validate the item has the expected attributes + assert returned_item["inspector_id_last4"] == "5678" + assert returned_item["unit"] == "011899988199" + break diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/virtual_beacon_searchable_encryption_example/__init__.py b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/virtual_beacon_searchable_encryption_example/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/virtual_beacon_searchable_encryption_example/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/virtual_beacon_searchable_encryption_example/beacon_config.py b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/virtual_beacon_searchable_encryption_example/beacon_config.py new file mode 100644 index 000000000..3713992e0 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/virtual_beacon_searchable_encryption_example/beacon_config.py @@ -0,0 +1,200 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Sets up the beacon config for virtual beacon searchable encryption.""" +import boto3 +from aws_cryptographic_material_providers.keystore.client import KeyStore +from aws_cryptographic_material_providers.keystore.config import KeyStoreConfig +from aws_cryptographic_material_providers.keystore.models import KMSConfigurationKmsKeyArn +from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders +from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig +from aws_cryptographic_material_providers.mpl.models import CreateAwsKmsHierarchicalKeyringInput +from aws_dbesdk_dynamodb.structures.dynamodb import ( + BeaconKeySourceSingle, + BeaconVersion, + DynamoDbTableEncryptionConfig, + DynamoDbTablesEncryptionConfig, + GetPrefix, + SearchConfig, + SingleKeyStore, + StandardBeacon, + VirtualField, + VirtualPart, + VirtualTransformPrefix, +) +from aws_dbesdk_dynamodb.structures.structured_encryption import CryptoAction + + +def setup_beacon_config( + ddb_table_name: str, + branch_key_id: str, + branch_key_wrapping_kms_key_arn: str, + branch_key_ddb_table_name: str, +): + """Set up the beacon config for virtual beacon searchable encryption.""" + # 1. Construct a length-1 prefix virtual transform. + # `hasTestResult` is a binary attribute, containing either `true` or `false`. + # As an example to demonstrate virtual transforms, we will truncate the value + # of `hasTestResult` in the virtual field to the length-1 prefix of the binary value, i.e.: + # - "true" -> "t" + # - "false -> "f" + # This is not necessary. This is done as a demonstration of virtual transforms. + # Virtual transform operations treat all attributes as strings + # (i.e. the boolean value `true` is interpreted as a string "true"), + # so its length-1 prefix is just "t". + length1_prefix_virtual_transform_list = [VirtualTransformPrefix(GetPrefix(length=1))] + + # 2. Construct the VirtualParts required for the VirtualField + has_test_result_part = VirtualPart( + loc="hasTestResult", + # Here, we apply the length-1 prefix virtual transform + trans=length1_prefix_virtual_transform_list, + ) + + state_part = VirtualPart( + loc="state", + # Note that we do not apply any transform to the `state` attribute, + # and the virtual field will read in the attribute as-is. + ) + + # 3. Construct the VirtualField from the VirtualParts + # Note that the order that virtual parts are added to the virtualPartList + # dictates the order in which they are concatenated to build the virtual field. + # You must add virtual parts in the same order on write as you do on read. + virtual_part_list = [state_part, has_test_result_part] + + state_and_has_test_result_field = VirtualField(name="stateAndHasTestResult", parts=virtual_part_list) + + virtual_field_list = [state_and_has_test_result_field] + + # 4. Configure our beacon. + # The virtual field is assumed to hold a US 2-letter state abbreviation + # (56 possible values = 50 states + 6 territories) concatenated with a binary attribute + # (2 possible values: true/false hasTestResult field), we expect a population size of + # 56 * 2 = 112 possible values. + # We will also assume that these values are reasonably well-distributed across + # customer IDs. In practice, this will not be true. We would expect + # more populous states to appear more frequently in the database. + # A more complex analysis would show that a stricter upper bound + # is necessary to account for this by hiding information from the + # underlying distribution. + # + # This link provides guidance for choosing a beacon length: + # https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/choosing-beacon-length.html + # We follow the guidance in the link above to determine reasonable bounds for beacon length: + # - min: log(sqrt(112))/log(2) ~= 3.4, round down to 3 + # - max: log((112/2))/log(2) ~= 5.8, round up to 6 + # You will somehow need to round results to a nearby integer. + # We choose to round to the nearest integer; you might consider a different rounding approach. + # Rounding up will return fewer expected "false positives" in queries, + # leading to fewer decrypt calls and better performance, + # but it is easier to identify which beacon values encode distinct plaintexts. + # Rounding down will return more expected "false positives" in queries, + # leading to more decrypt calls and worse performance, + # but it is harder to identify which beacon values encode distinct plaintexts. + # We can choose a beacon length between 3 and 6: + # - Closer to 3, we expect more "false positives" to be returned, + # making it harder to identify which beacon values encode distinct plaintexts, + # but leading to more decrypt calls and worse performance + # - Closer to 6, we expect fewer "false positives" returned in queries, + # leading to fewer decrypt calls and better performance, + # but it is easier to identify which beacon values encode distinct plaintexts. + # As an example, we will choose 5. + # Values stored in aws_dbe_b_stateAndHasTestResult will be 5 bits long (0x00 - 0x1f) + # There will be 2^5 = 32 possible HMAC values. + # With a well-distributed dataset (112 values), for a particular beacon we expect + # (112/32) = 3.5 combinations of abbreviation + true/false attribute + # sharing that beacon value. + standard_beacon_list = [ + StandardBeacon( + # This name is the same as our virtual field's name above + name="stateAndHasTestResult", + length=5, + ) + ] + + # 5. Configure Keystore. + # This example expects that you have already set up a KeyStore with a single branch key. + # See the "CreateKeyStoreTableExample" and "CreateKeyStoreKeyExample" files for how to do this. + # After you create a branch key, you should persist its ID for use in this example. + keystore = KeyStore( + config=KeyStoreConfig( + ddb_client=boto3.client("dynamodb"), + ddb_table_name=branch_key_ddb_table_name, + logical_key_store_name=branch_key_ddb_table_name, + kms_client=boto3.client("kms"), + kms_configuration=KMSConfigurationKmsKeyArn(value=branch_key_wrapping_kms_key_arn), + ) + ) + + # 6. Create BeaconVersion. + # The BeaconVersion inside the list holds the list of beacons on the table. + # The BeaconVersion also stores information about the keystore. + # BeaconVersion must be provided: + # - keyStore: The keystore configured in the previous step. + # - keySource: A configuration for the key source. + # For simple use cases, we can configure a 'singleKeySource' which + # statically configures a single beaconKey. That is the approach this example takes. + # For use cases where you want to use different beacon keys depending on the data + # (for example if your table holds data for multiple tenants, and you want to use + # a different beacon key per tenant), look into configuring a MultiKeyStore: + # https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/searchable-encryption-multitenant.html + # We also provide our standard beacon list and virtual fields here. + beacon_versions = [ + BeaconVersion( + virtual_fields=virtual_field_list, + standard_beacons=standard_beacon_list, + version=1, # MUST be 1 + key_store=keystore, + key_source=BeaconKeySourceSingle( + SingleKeyStore( + # `key_id` references a beacon key. + # For every branch key we create in the keystore, + # we also create a beacon key. + # This beacon key is not the same as the branch key, + # but is created with the same ID as the branch key. + key_id=branch_key_id, + cache_ttl=6000, + ) + ), + ) + ] + + # 7. Create a Hierarchical Keyring + # This is a KMS keyring that utilizes the keystore table. + # This config defines how items are encrypted and decrypted. + # NOTE: You should configure this to use the same keystore as your search config. + mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig()) + + keyring_input = CreateAwsKmsHierarchicalKeyringInput( + branch_key_id=branch_key_id, key_store=keystore, ttl_seconds=6000 + ) + + kms_keyring = mat_prov.create_aws_kms_hierarchical_keyring(input=keyring_input) + + # 8. 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 + # Any attributes that will be used in beacons must be configured as ENCRYPT_AND_SIGN. + attribute_actions = { + "customer_id": CryptoAction.SIGN_ONLY, # Our partition attribute must be SIGN_ONLY + "create_time": CryptoAction.SIGN_ONLY, # Our sort attribute must be SIGN_ONLY + "state": CryptoAction.ENCRYPT_AND_SIGN, # Beaconized attributes must be encrypted + "hasTestResult": CryptoAction.ENCRYPT_AND_SIGN, # Beaconized attributes must be encrypted + } + + # 9. Create the DynamoDb Encryption configuration for the table we will be writing to. + # The beaconVersions are added to the search configuration. + table_config = DynamoDbTableEncryptionConfig( + logical_table_name=ddb_table_name, + partition_key_name="customer_id", + sort_key_name="create_time", + attribute_actions_on_encrypt=attribute_actions, + keyring=kms_keyring, + search=SearchConfig(write_version=1, versions=beacon_versions), # MUST be 1 + ) + + table_configs = {ddb_table_name: table_config} + return DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/virtual_beacon_searchable_encryption_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/virtual_beacon_searchable_encryption_example/with_encrypted_client.py similarity index 52% rename from Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/virtual_beacon_searchable_encryption_example.py rename to Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/virtual_beacon_searchable_encryption_example/with_encrypted_client.py index 681f660cc..bae7a1a39 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/virtual_beacon_searchable_encryption_example.py +++ b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/virtual_beacon_searchable_encryption_example/with_encrypted_client.py @@ -1,7 +1,9 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 """ -Example demonstrating DynamoDB encryption using virtual beacons. +Example demonstrating DynamoDB encryption using virtual beacons with EncryptedClient. This example demonstrates how to set up a virtual field from two DDB attributes, create a standard beacon with that field, put an item with @@ -95,12 +97,6 @@ import time import boto3 -from aws_cryptographic_material_providers.keystore.client import KeyStore -from aws_cryptographic_material_providers.keystore.config import KeyStoreConfig -from aws_cryptographic_material_providers.keystore.models import KMSConfigurationKmsKeyArn -from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders -from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig -from aws_cryptographic_material_providers.mpl.models import CreateAwsKmsHierarchicalKeyringInput from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_transforms.client import ( DynamoDbEncryptionTransforms, @@ -108,205 +104,39 @@ from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_transforms.models import ( ResolveAttributesInput, ) -from aws_dbesdk_dynamodb.structures.dynamodb import ( - BeaconKeySourceSingle, - BeaconVersion, - DynamoDbTableEncryptionConfig, - DynamoDbTablesEncryptionConfig, - GetPrefix, - SearchConfig, - SingleKeyStore, - StandardBeacon, - VirtualField, - VirtualPart, - VirtualTransformPrefix, + +from DynamoDBEncryption.src.searchable_encryption.virtual_beacon_searchable_encryption_example.beacon_config import ( + setup_beacon_config, ) -from aws_dbesdk_dynamodb.structures.structured_encryption import CryptoAction GSI_NAME = "stateAndHasTestResult-index" -def put_item_query_item_with_virtual_beacon( - ddb_table_name: str, branch_key_id: str, branch_key_wrapping_kms_key_arn: str, branch_key_ddb_table_name: str +def virtual_beacon_client_example( + ddb_table_name: str, + branch_key_id: str, + branch_key_wrapping_kms_key_arn: str, + branch_key_ddb_table_name: str, ): """ - Demonstrate using virtual beacons with DynamoDB encryption. + Demonstrate using virtual beacon searchable encryption with EncryptedClient. :param ddb_table_name: The name of the DynamoDB table :param branch_key_id: Branch key ID for a branch key previously created in key store :param branch_key_wrapping_kms_key_arn: ARN of KMS key used to create the branch key :param branch_key_ddb_table_name: Name of DDB table representing the branch key store """ - # 1. Construct a length-1 prefix virtual transform. - # `hasTestResult` is a binary attribute, containing either `true` or `false`. - # As an example to demonstrate virtual transforms, we will truncate the value - # of `hasTestResult` in the virtual field to the length-1 prefix of the binary value, i.e.: - # - "true" -> "t" - # - "false -> "f" - # This is not necessary. This is done as a demonstration of virtual transforms. - # Virtual transform operations treat all attributes as strings - # (i.e. the boolean value `true` is interpreted as a string "true"), - # so its length-1 prefix is just "t". - length1_prefix_virtual_transform_list = [VirtualTransformPrefix(GetPrefix(length=1))] - - # 2. Construct the VirtualParts required for the VirtualField - has_test_result_part = VirtualPart( - loc="hasTestResult", - # Here, we apply the length-1 prefix virtual transform - trans=length1_prefix_virtual_transform_list, - ) - - state_part = VirtualPart( - loc="state", - # Note that we do not apply any transform to the `state` attribute, - # and the virtual field will read in the attribute as-is. - ) - - # 3. Construct the VirtualField from the VirtualParts - # Note that the order that virtual parts are added to the virtualPartList - # dictates the order in which they are concatenated to build the virtual field. - # You must add virtual parts in the same order on write as you do on read. - virtual_part_list = [state_part, has_test_result_part] - - state_and_has_test_result_field = VirtualField(name="stateAndHasTestResult", parts=virtual_part_list) - - virtual_field_list = [state_and_has_test_result_field] - - # 4. Configure our beacon. - # The virtual field is assumed to hold a US 2-letter state abbreviation - # (56 possible values = 50 states + 6 territories) concatenated with a binary attribute - # (2 possible values: true/false hasTestResult field), we expect a population size of - # 56 * 2 = 112 possible values. - # We will also assume that these values are reasonably well-distributed across - # customer IDs. In practice, this will not be true. We would expect - # more populous states to appear more frequently in the database. - # A more complex analysis would show that a stricter upper bound - # is necessary to account for this by hiding information from the - # underlying distribution. - # - # This link provides guidance for choosing a beacon length: - # https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/choosing-beacon-length.html - # We follow the guidance in the link above to determine reasonable bounds for beacon length: - # - min: log(sqrt(112))/log(2) ~= 3.4, round down to 3 - # - max: log((112/2))/log(2) ~= 5.8, round up to 6 - # You will somehow need to round results to a nearby integer. - # We choose to round to the nearest integer; you might consider a different rounding approach. - # Rounding up will return fewer expected "false positives" in queries, - # leading to fewer decrypt calls and better performance, - # but it is easier to identify which beacon values encode distinct plaintexts. - # Rounding down will return more expected "false positives" in queries, - # leading to more decrypt calls and worse performance, - # but it is harder to identify which beacon values encode distinct plaintexts. - # We can choose a beacon length between 3 and 6: - # - Closer to 3, we expect more "false positives" to be returned, - # making it harder to identify which beacon values encode distinct plaintexts, - # but leading to more decrypt calls and worse performance - # - Closer to 6, we expect fewer "false positives" returned in queries, - # leading to fewer decrypt calls and better performance, - # but it is easier to identify which beacon values encode distinct plaintexts. - # As an example, we will choose 5. - # Values stored in aws_dbe_b_stateAndHasTestResult will be 5 bits long (0x00 - 0x1f) - # There will be 2^5 = 32 possible HMAC values. - # With a well-distributed dataset (112 values), for a particular beacon we expect - # (112/32) = 3.5 combinations of abbreviation + true/false attribute - # sharing that beacon value. - standard_beacon_list = [ - StandardBeacon( - # This name is the same as our virtual field's name above - name="stateAndHasTestResult", - length=5, - ) - ] - - # 5. Configure Keystore. - # This example expects that you have already set up a KeyStore with a single branch key. - # See the "CreateKeyStoreTableExample" and "CreateKeyStoreKeyExample" files for how to do this. - # After you create a branch key, you should persist its ID for use in this example. - keystore = KeyStore( - config=KeyStoreConfig( - ddb_client=boto3.client("dynamodb"), - ddb_table_name=branch_key_ddb_table_name, - logical_key_store_name=branch_key_ddb_table_name, - kms_client=boto3.client("kms"), - kms_configuration=KMSConfigurationKmsKeyArn(value=branch_key_wrapping_kms_key_arn), - ) - ) - - # 6. Create BeaconVersion. - # The BeaconVersion inside the list holds the list of beacons on the table. - # The BeaconVersion also stores information about the keystore. - # BeaconVersion must be provided: - # - keyStore: The keystore configured in the previous step. - # - keySource: A configuration for the key source. - # For simple use cases, we can configure a 'singleKeySource' which - # statically configures a single beaconKey. That is the approach this example takes. - # For use cases where you want to use different beacon keys depending on the data - # (for example if your table holds data for multiple tenants, and you want to use - # a different beacon key per tenant), look into configuring a MultiKeyStore: - # https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/searchable-encryption-multitenant.html - # We also provide our standard beacon list and virtual fields here. - beacon_versions = [ - BeaconVersion( - virtual_fields=virtual_field_list, - standard_beacons=standard_beacon_list, - version=1, # MUST be 1 - key_store=keystore, - key_source=BeaconKeySourceSingle( - SingleKeyStore( - # `key_id` references a beacon key. - # For every branch key we create in the keystore, - # we also create a beacon key. - # This beacon key is not the same as the branch key, - # but is created with the same ID as the branch key. - key_id=branch_key_id, - cache_ttl=6000, - ) - ), - ) - ] - - # 7. Create a Hierarchical Keyring - # This is a KMS keyring that utilizes the keystore table. - # This config defines how items are encrypted and decrypted. - # NOTE: You should configure this to use the same keystore as your search config. - mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig()) - - keyring_input = CreateAwsKmsHierarchicalKeyringInput( - branch_key_id=branch_key_id, key_store=keystore, ttl_seconds=6000 + # 1. Create the DynamoDb Encryption configuration for the table we will be writing to. + # See beacon_config.py in this directory for detailed steps on the encryption configuration. + tables_config = setup_beacon_config( + ddb_table_name, branch_key_id, branch_key_wrapping_kms_key_arn, branch_key_ddb_table_name ) - kms_keyring = mat_prov.create_aws_kms_hierarchical_keyring(input=keyring_input) - - # 8. 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 - # Any attributes that will be used in beacons must be configured as ENCRYPT_AND_SIGN. - attribute_actions = { - "customer_id": CryptoAction.SIGN_ONLY, # Our partition attribute must be SIGN_ONLY - "create_time": CryptoAction.SIGN_ONLY, # Our sort attribute must be SIGN_ONLY - "state": CryptoAction.ENCRYPT_AND_SIGN, # Beaconized attributes must be encrypted - "hasTestResult": CryptoAction.ENCRYPT_AND_SIGN, # Beaconized attributes must be encrypted - } - - # 9. Create the DynamoDb Encryption configuration for the table we will be writing to. - # The beaconVersions are added to the search configuration. - table_config = DynamoDbTableEncryptionConfig( - logical_table_name=ddb_table_name, - partition_key_name="customer_id", - sort_key_name="create_time", - attribute_actions_on_encrypt=attribute_actions, - keyring=kms_keyring, - search=SearchConfig(write_version=1, versions=beacon_versions), # MUST be 1 - ) - - table_configs = {ddb_table_name: table_config} - tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs) - - # 10. Create test items + # 2. Create the EncryptedClient + ddb_client = boto3.client("dynamodb") + encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config) + # 3. Create test items - one with hasTestResult=true and one with hasTestResult=false # Create item with hasTestResult=true item_with_has_test_result = { "customer_id": {"S": "ABC-123"}, @@ -323,7 +153,7 @@ def put_item_query_item_with_virtual_beacon( "hasTestResult": {"BOOL": False}, } - # 11. If developing or debugging, verify config by checking virtual field values directly + # 4. If developing or debugging, verify config by checking virtual field values directly trans = DynamoDbEncryptionTransforms(config=tables_config) resolve_input = ResolveAttributesInput(table_name=ddb_table_name, item=item_with_has_test_result, version=1) @@ -337,11 +167,7 @@ def put_item_query_item_with_virtual_beacon( vf = {"stateAndHasTestResult": "CAt"} assert resolve_output.virtual_fields == vf - # 12. Create the EncryptedClient - ddb_client = boto3.client("dynamodb") - encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config) - - # 13. Put two items into our table using the above client. + # 5. Put two items into our table using the above client. # The two items will differ only in their `customer_id` attribute (primary key) # and their `hasTestResult` attribute. # We will query against these items to demonstrate how to use our setup above @@ -361,7 +187,7 @@ def put_item_query_item_with_virtual_beacon( # Assert PutItem was successful assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 - # 14. Query by stateAndHasTestResult attribute. + # 6. Query by stateAndHasTestResult attribute. # Note that we are constructing the query as if we were querying on plaintext values. # However, the DDB encryption client will detect that this attribute name has a beacon configured. # The client will add the beaconized attribute name and attribute value to the query, @@ -385,6 +211,12 @@ def put_item_query_item_with_virtual_beacon( # we write that field as its length-1 prefix in the query. expression_attribute_values = {":stateAndHasTestResult": {"S": "CAt"}} + # We are querying for the item with state="CA" and hasTestResult=true. + # We constructed our virtual field as state+hasTestResult, + # with hasTestResult truncated to its first character. + # For "true", this is "t", so we search for "CAt" + expression_attribute_values = {":stateAndHasTestResult": {"S": "CAt"}} + # GSIs do not update instantly # so if the results come back empty # we retry after a short sleep diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/virtual_beacon_searchable_encryption_example/with_encrypted_table.py b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/virtual_beacon_searchable_encryption_example/with_encrypted_table.py new file mode 100644 index 000000000..fa74cd773 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/virtual_beacon_searchable_encryption_example/with_encrypted_table.py @@ -0,0 +1,240 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Example demonstrating DynamoDB encryption using virtual beacons with EncryptedTable. + +This example demonstrates how to set up a virtual field from two DDB +attributes, create a standard beacon with that field, put an item with +that beacon, and query against that beacon. + +A virtual field is a field consisting of a transformation of one or more attributes in a DDB item. +Virtual fields are useful in querying against encrypted fields that only have a handful of +possible values. They allow you to take fields with few possible values, concatenate +them to other fields, then query against the combined field. This enables using these types of +fields in queries while making it infeasible to identify which beacon values encode +the few possible distinct plaintexts. This is explained in more detail below. +Virtual fields are not stored in the DDB table. However, they are used to construct +a beacon, the value of which is stored. + +For more information on virtual fields, see + https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/beacons.html#virtual-field + +For our example, we will construct a virtual field +from two DDB attributes `state` and `hasTestResult` as `state`+prefix(`hasTestResult`, 1). +We will then create a beacon out of this virtual field and use it to search. + +This example follows a use case of a database that stores customer test result metadata. +Records are indexed by `customer_id` and store a `state` attribute, representing the +US state or territory where the customer lives, and a `hasTestResult` boolean attribute, +representing whether the customer has a "test result" available. (Maybe this represents +some medical test result, and this table stores "result available" metadata.) We assume +that values in these fields are uniformly distributed across all possible values for +these fields (56 for `state`, 2 for `hasTestResult`), and are uniformly distributed across +customer IDs. + +The motivation behind this example is to demonstrate how and why one would use a virtual beacon. +In this example, our table stores records with an encrypted boolean `hasTestResult` attribute. +We would like to be able to query for customers in a given state with a `true` hasTestResult +attribute. + +To be able to execute this query securely and efficiently, we want the following +properties on our table: + 1. Hide the distribution of `hasTestResult` attribute values (i.e. it should be infeasible + to determine the percentage of `true`s to `false`s across the dataset from beaconized + values) + 2. Query against a combination of whether `hasTestResult` is true/false and the `state` field +We cannot achieve these properties with a standard beacon on a true/false attribute. Following +the guidance to choose a beacon length: + https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/choosing-beacon-length.html +For a boolean value (in our case, whether `hasTestResult` is true or false), the acceptable +bounds for beacon length are either 0 or 1. This corresponds to either not storing a beacon +(length 0), or effectively storing another boolean attribute (length 1). With +length 0, this beacon is useless for searching (violating property 2); with length 1, this +beacon may not hide the attribute (violating property 1). +In addition, choosing a longer beacon length does not help us. +Each attribute value is mapped to a distinct beacon. +Since booleans only have 2 possible attribute values, we will still only have 2 possible +beacon values, though those values may be longer. A longer beacon provides no advantages over +beacon of length 1 in this situation. + +A compound beacon also does not help. +To (over)simplify, a compound beacon is a concatenation of standard beacons, +i.e. beacon(`state`)+beacon(`hasTestResult`). +The `hasTestResult` beacon is still visible, so we would still have the problems above. + +To achieve these properties, we instead construct a virtual field and use that in our beacon, +i.e. beacon(`state`+`hasTestResult`). Assuming these fields are well-distributed across +customer IDs and possible values, this gives us both desired properties; we can query against +both attributes while hiding information from the underlying data. This is demonstrated in more +detail below. + +Running this example requires access to a DDB table with the +following primary key configuration: + - Partition key is named "customer_id" with type (S) + - Sort key is named "create_time" with type (S) +This table must have a Global Secondary Index (GSI) configured named "stateAndHasTestResult-index": + - Partition key is named "aws_dbe_b_stateAndHasTestResult" with type (S) + +In this example for storing customer location data, this schema is utilized for the data: + - "customer_id" stores a unique customer identifier + - "create_time" stores a Unix timestamp + - "state" stores an encrypted 2-letter US state or territory abbreviation + (https://www.faa.gov/air_traffic/publications/atpubs/cnt_html/appendix_a.html) + - "hasTestResult" is not part of the schema, but is an attribute utilized in this example. + It stores a boolean attribute (false/true) indicating whether this customer has a test result + available. + +The example requires the following ordered input command line parameters: + 1. DDB table name for table to put/query data from + 2. Branch key ID for a branch key that was previously created in your key store. See the + CreateKeyStoreKeyExample. + 3. Branch key wrapping KMS key ARN for the KMS key used to create the branch key with ID + provided in arg 2 + 4. Branch key DDB table name for the DDB table representing the branch key store +""" +import time + +import boto3 +from aws_dbesdk_dynamodb.encrypted.table import EncryptedTable +from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_transforms.client import ( + DynamoDbEncryptionTransforms, +) +from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_transforms.models import ( + ResolveAttributesInput, +) + +from DynamoDBEncryption.src.searchable_encryption.virtual_beacon_searchable_encryption_example.beacon_config import ( + setup_beacon_config, +) + +GSI_NAME = "stateAndHasTestResult-index" + + +def virtual_beacon_table_example( + ddb_table_name: str, + branch_key_id: str, + branch_key_wrapping_kms_key_arn: str, + branch_key_ddb_table_name: str, +): + """ + Demonstrate using virtual beacon searchable encryption with EncryptedTable. + + :param ddb_table_name: The name of the DynamoDB table + :param branch_key_id: Branch key ID for a branch key previously created in key store + :param branch_key_wrapping_kms_key_arn: ARN of KMS key used to create the branch key + :param branch_key_ddb_table_name: Name of DDB table representing the branch key store + """ + # 1. Create the DynamoDb Encryption configuration using the setup_beacon_config function + # See beacon_config.py in this directory for detailed steps on the encryption configuration. + tables_config = setup_beacon_config( + ddb_table_name, branch_key_id, branch_key_wrapping_kms_key_arn, branch_key_ddb_table_name + ) + + # 2. Create the EncryptedTable + ddb_table = boto3.resource("dynamodb").Table(ddb_table_name) + encrypted_table = EncryptedTable(table=ddb_table, encryption_config=tables_config) + + # 3. Create test items - one with hasTestResult=true and one with hasTestResult=false + # Create item with hasTestResult=true + item_with_has_test_result = { + "customer_id": "ABC-123", + "create_time": 1681495205, + "state": "CA", + "hasTestResult": True, + } + + # Create item with hasTestResult=false + item_with_no_has_test_result = { + "customer_id": "DEF-456", + "create_time": 1681495205, + "state": "CA", + "hasTestResult": False, + } + + # # 4. If developing or debugging, verify config by checking virtual field values directly + # trans = DynamoDbEncryptionTransforms(config=tables_config) + # + # resolve_input = ResolveAttributesInput(table_name=ddb_table_name, item=item_with_has_test_result, version=1) + # + # resolve_output = trans.resolve_attributes(input=resolve_input) + # + # # CompoundBeacons is empty because we have no Compound Beacons configured + # assert not resolve_output.compound_beacons + # + # # Verify that VirtualFields has the expected value + # vf = {"stateAndHasTestResult": "CAt"} + # assert resolve_output.virtual_fields == vf + + # 5. Put two items into our table using the above encrypted table. + # The two items will differ only in their `customer_id` attribute (primary key) + # and their `hasTestResult` attribute. + # We will query against these items to demonstrate how to use our setup above + # to query against our `stateAndHasTestResult` beacon. + # Before the item gets sent to DynamoDb, it will be encrypted + # client-side, according to our configuration. + # Since our configuration includes a beacon on a virtual field named + # `stateAndHasTestResult`, the client will add an attribute + # to the item with name `aws_dbe_b_stateAndHasTestResult`. + # Its value will be an HMAC truncated to as many bits as the + # beacon's `length` parameter; i.e. 5. + put_response = encrypted_table.put_item(Item=item_with_has_test_result) + # Assert PutItem was successful + assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + put_response = encrypted_table.put_item(Item=item_with_no_has_test_result) + # Assert PutItem was successful + assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + # 6. Query by stateAndHasTestResult attribute. + # Note that we are constructing the query as if we were querying on plaintext values. + # However, the DDB encryption client will detect that this attribute name has a beacon configured. + # The client will add the beaconized attribute name and attribute value to the query, + # and transform the query to use the beaconized name and value. + # Internally, the client will query for and receive all items with a matching HMAC value in the beacon field. + # This may include a number of "false positives" with different ciphertext, but the same truncated HMAC. + # e.g. if truncate(HMAC("CAt"), 5) == truncate(HMAC("DCf"), 5), the query will return both items. + # The client will decrypt all returned items to determine which ones have the expected attribute values, + # and only surface items with the correct plaintext to the user. + # This procedure is internal to the client and is abstracted away from the user; + # e.g. the user will only see "CAt" and never "DCf", though the actual query returned both. + expression_attribute_names = {"#stateAndHasTestResult": "stateAndHasTestResult"} + + # We are querying for the item with `state`="CA" and `hasTestResult`=`true`. + # Since we added virtual parts as `state` then `hasTestResult`, + # we must write our query expression in the same order. + # We constructed our virtual field as `state`+`hasTestResult`, + # so we add the two parts in that order. + # Since we also created a virtual transform that truncated `hasTestResult` + # to its length-1 prefix, i.e. "true" -> "t", + # we write that field as its length-1 prefix in the query. + expression_attribute_values = {":stateAndHasTestResult": "CAt"} + + # GSIs do not update instantly + # so if the results come back empty + # we retry after a short sleep + for _ in range(10): + query_response = encrypted_table.query( + IndexName=GSI_NAME, + KeyConditionExpression="#stateAndHasTestResult = :stateAndHasTestResult", + ExpressionAttributeNames=expression_attribute_names, + ExpressionAttributeValues=expression_attribute_values, + ) + + # Validate query was returned successfully + assert query_response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + items = query_response.get("Items", []) + # if no results, sleep and try again + if not items: + time.sleep(0.02) + continue + + # Validate only 1 item was returned: the item with the expected attributes + assert len(items) == 1 + returned_item = items[0] + # Validate the item has the expected attributes + assert returned_item["state"] == "CA" + assert returned_item["hasTestResult"] is True + break diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/hierarchical_keyring_example/__init__.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/hierarchical_keyring_example/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/hierarchical_keyring_example/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/hierarchical_keyring_example/test_with_encrypted_client.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/hierarchical_keyring_example/test_with_encrypted_client.py new file mode 100644 index 000000000..8f9ac7524 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/hierarchical_keyring_example/test_with_encrypted_client.py @@ -0,0 +1,46 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test hierarchical keyring with abstraction EncryptedClient example.""" +import time + +import pytest + +from ....src.create_keystore_key_example import keystore_create_key +from ....src.keyring.hierarchical_keyring_example.with_encrypted_client import ( + hierarchical_keyring_client_example, +) +from ...cleanup import delete_branch_key +from ...test_utils import ( + TEST_DDB_TABLE_NAME, + TEST_KEYSTORE_KMS_KEY_ID, + TEST_KEYSTORE_NAME, + TEST_LOGICAL_KEYSTORE_NAME, +) + +pytestmark = [pytest.mark.examples] + + +def test_hierarchical_keyring_client_example(): + """Test hierarchical keyring abstracted client example.""" + # Create new branch keys for test + key_id1 = keystore_create_key(TEST_KEYSTORE_NAME, TEST_LOGICAL_KEYSTORE_NAME, TEST_KEYSTORE_KMS_KEY_ID) + key_id2 = keystore_create_key(TEST_KEYSTORE_NAME, TEST_LOGICAL_KEYSTORE_NAME, TEST_KEYSTORE_KMS_KEY_ID) + + try: + # Key creation is eventually consistent, so wait 5 seconds to decrease the likelihood + # our test fails due to eventual consistency issues. + time.sleep(5) + + # Run the client example + hierarchical_keyring_client_example( + TEST_DDB_TABLE_NAME, + key_id1, + key_id2, + TEST_KEYSTORE_NAME, + TEST_LOGICAL_KEYSTORE_NAME, + TEST_KEYSTORE_KMS_KEY_ID, + ) + finally: + # Cleanup Branch Keys + delete_branch_key(key_id1, TEST_KEYSTORE_NAME, None) + delete_branch_key(key_id2, TEST_KEYSTORE_NAME, None) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/hierarchical_keyring_example/test_with_encrypted_table.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/hierarchical_keyring_example/test_with_encrypted_table.py new file mode 100644 index 000000000..bc0c51d78 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/hierarchical_keyring_example/test_with_encrypted_table.py @@ -0,0 +1,46 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test hierarchical keyring with abstraction EncryptedTable example.""" +import time + +import pytest + +from ....src.create_keystore_key_example import keystore_create_key +from ....src.keyring.hierarchical_keyring_example.with_encrypted_table import ( + hierarchical_keyring_table_example, +) +from ...cleanup import delete_branch_key +from ...test_utils import ( + TEST_DDB_TABLE_NAME, + TEST_KEYSTORE_KMS_KEY_ID, + TEST_KEYSTORE_NAME, + TEST_LOGICAL_KEYSTORE_NAME, +) + +pytestmark = [pytest.mark.examples] + + +def test_hierarchical_keyring_table_example(): + """Test hierarchical keyring abstracted table example.""" + # Create new branch keys for test + key_id1 = keystore_create_key(TEST_KEYSTORE_NAME, TEST_LOGICAL_KEYSTORE_NAME, TEST_KEYSTORE_KMS_KEY_ID) + key_id2 = keystore_create_key(TEST_KEYSTORE_NAME, TEST_LOGICAL_KEYSTORE_NAME, TEST_KEYSTORE_KMS_KEY_ID) + + try: + # Key creation is eventually consistent, so wait 5 seconds to decrease the likelihood + # our test fails due to eventual consistency issues. + time.sleep(5) + + # Run the table example + hierarchical_keyring_table_example( + TEST_DDB_TABLE_NAME, + key_id1, + key_id2, + TEST_KEYSTORE_NAME, + TEST_LOGICAL_KEYSTORE_NAME, + TEST_KEYSTORE_KMS_KEY_ID, + ) + finally: + # Cleanup Branch Keys + delete_branch_key(key_id1, TEST_KEYSTORE_NAME, None) + delete_branch_key(key_id2, TEST_KEYSTORE_NAME, None) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/kms_ecdh_keyring_example/__init__.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/kms_ecdh_keyring_example/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/kms_ecdh_keyring_example/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/kms_ecdh_keyring_example/test_with_encrypted_client.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/kms_ecdh_keyring_example/test_with_encrypted_client.py new file mode 100644 index 000000000..9e3658d71 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/kms_ecdh_keyring_example/test_with_encrypted_client.py @@ -0,0 +1,50 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test KMS ECDH keyring with encrypted client example.""" +import pytest + +from ....src.keyring.kms_ecdh_keyring_example.utility import ( + EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME, + EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME, + should_get_new_public_keys, + write_public_key_pem_for_ecc_key, +) +from ....src.keyring.kms_ecdh_keyring_example.with_encrypted_client import ( + kms_ecdh_discovery_client_example, + kms_ecdh_keyring_client_example, +) +from ...test_utils import TEST_DDB_TABLE_NAME, TEST_KMS_ECDH_KEY_ID_P256_RECIPIENT, TEST_KMS_ECDH_KEY_ID_P256_SENDER + +pytestmark = [pytest.mark.examples] + + +def test_kms_ecdh_keyring_client_example_static(): + """Test kms_ecdh_keyring_client_example with static configuration.""" + # You may provide your own ECC public keys at + # - EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME + # - EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME. + # If you provide these, the keys MUST be on curve P256 + # This must be the public key for the ECC key represented at eccKeyArn + # If this file is not present, this will write a UTF-8 encoded PEM file for you. + if should_get_new_public_keys(): + write_public_key_pem_for_ecc_key(TEST_KMS_ECDH_KEY_ID_P256_SENDER, EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME) + write_public_key_pem_for_ecc_key(TEST_KMS_ECDH_KEY_ID_P256_RECIPIENT, EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME) + + kms_ecdh_keyring_client_example(TEST_DDB_TABLE_NAME, TEST_KMS_ECDH_KEY_ID_P256_SENDER) + + +def test_kms_ecdh_keyring_client_example_discovery(): + """Test kms_ecdh_keyring_client_example with discovery configuration.""" + # In this example you do not need to provide the recipient ECC Public Key. + # On initialization, the keyring will call KMS:getPublicKey on the configured + # recipientKmsIdentifier set on the keyring. This example uses the previous example + # to write an item meant for the recipient. + # First, we need to run the static example to write the item + if should_get_new_public_keys(): + write_public_key_pem_for_ecc_key(TEST_KMS_ECDH_KEY_ID_P256_SENDER, EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME) + write_public_key_pem_for_ecc_key(TEST_KMS_ECDH_KEY_ID_P256_RECIPIENT, EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME) + + kms_ecdh_keyring_client_example(TEST_DDB_TABLE_NAME, TEST_KMS_ECDH_KEY_ID_P256_SENDER) + + # Now test the discovery example + kms_ecdh_discovery_client_example(TEST_DDB_TABLE_NAME, TEST_KMS_ECDH_KEY_ID_P256_RECIPIENT) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/kms_ecdh_keyring_example/test_with_encrypted_table.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/kms_ecdh_keyring_example/test_with_encrypted_table.py new file mode 100644 index 000000000..0be8f4c1e --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/kms_ecdh_keyring_example/test_with_encrypted_table.py @@ -0,0 +1,50 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test KMS ECDH keyring with encrypted table example.""" +import pytest + +from ....src.keyring.kms_ecdh_keyring_example.utility import ( + EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME, + EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME, + should_get_new_public_keys, + write_public_key_pem_for_ecc_key, +) +from ....src.keyring.kms_ecdh_keyring_example.with_encrypted_table import ( + kms_ecdh_discovery_table_example, + kms_ecdh_keyring_table_example, +) +from ...test_utils import TEST_DDB_TABLE_NAME, TEST_KMS_ECDH_KEY_ID_P256_RECIPIENT, TEST_KMS_ECDH_KEY_ID_P256_SENDER + +pytestmark = [pytest.mark.examples] + + +def test_kms_ecdh_keyring_table_example_static(): + """Test kms_ecdh_keyring_table_example with static configuration.""" + # You may provide your own ECC public keys at + # - EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME + # - EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME. + # If you provide these, the keys MUST be on curve P256 + # This must be the public key for the ECC key represented at eccKeyArn + # If this file is not present, this will write a UTF-8 encoded PEM file for you. + if should_get_new_public_keys(): + write_public_key_pem_for_ecc_key(TEST_KMS_ECDH_KEY_ID_P256_SENDER, EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME) + write_public_key_pem_for_ecc_key(TEST_KMS_ECDH_KEY_ID_P256_RECIPIENT, EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME) + + kms_ecdh_keyring_table_example(TEST_DDB_TABLE_NAME, TEST_KMS_ECDH_KEY_ID_P256_SENDER) + + +def test_kms_ecdh_keyring_table_example_discovery(): + """Test kms_ecdh_keyring_table_example with discovery configuration.""" + # In this example you do not need to provide the recipient ECC Public Key. + # On initialization, the keyring will call KMS:getPublicKey on the configured + # recipientKmsIdentifier set on the keyring. This example uses the previous example + # to write an item meant for the recipient. + # First, we need to run the static example to write the item + if should_get_new_public_keys(): + write_public_key_pem_for_ecc_key(TEST_KMS_ECDH_KEY_ID_P256_SENDER, EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME) + write_public_key_pem_for_ecc_key(TEST_KMS_ECDH_KEY_ID_P256_RECIPIENT, EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME) + + kms_ecdh_keyring_table_example(TEST_DDB_TABLE_NAME, TEST_KMS_ECDH_KEY_ID_P256_SENDER) + + # Now test the discovery example + kms_ecdh_discovery_table_example(TEST_DDB_TABLE_NAME, TEST_KMS_ECDH_KEY_ID_P256_RECIPIENT) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/kms_rsa_keyring_example/__init__.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/kms_rsa_keyring_example/__init__.py new file mode 100644 index 000000000..f459ac92f --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/kms_rsa_keyring_example/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""KMS RSA keyring examples test package.""" diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/kms_rsa_keyring_example/test_with_encrypted_client.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/kms_rsa_keyring_example/test_with_encrypted_client.py new file mode 100644 index 000000000..be06d3f0a --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/kms_rsa_keyring_example/test_with_encrypted_client.py @@ -0,0 +1,27 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test KMS RSA keyring with encrypted client example.""" +import pytest + +from ....src.keyring.kms_rsa_keyring_example.utility import ( + should_get_new_public_key, + write_public_key_pem_for_rsa_key, +) +from ....src.keyring.kms_rsa_keyring_example.with_encrypted_client import kms_rsa_keyring_client_example +from ...test_utils import ( + TEST_DDB_TABLE_NAME, + TEST_KMS_RSA_KEY_ID, +) + +pytestmark = [pytest.mark.examples] + + +def test_kms_rsa_keyring_client_example(): + """Test the KMS RSA keyring client example.""" + # You may provide your own RSA public key at EXAMPLE_RSA_PUBLIC_KEY_FILENAME. + # This must be the public key for the RSA key represented at rsa_key_arn. + # If this file is not present, this will write a UTF-8 encoded PEM file for you. + if should_get_new_public_key(): + write_public_key_pem_for_rsa_key(TEST_KMS_RSA_KEY_ID) + + kms_rsa_keyring_client_example(TEST_DDB_TABLE_NAME, TEST_KMS_RSA_KEY_ID) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_kms_rsa_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/kms_rsa_keyring_example/test_with_encrypted_table.py similarity index 60% rename from Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_kms_rsa_keyring_example.py rename to Examples/runtimes/python/DynamoDBEncryption/test/keyring/kms_rsa_keyring_example/test_with_encrypted_table.py index 908531d5f..cf3da8627 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_kms_rsa_keyring_example.py +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/kms_rsa_keyring_example/test_with_encrypted_table.py @@ -1,14 +1,14 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -"""Test for the KMS RSA keyring example.""" +"""Test KMS RSA keyring with encrypted table example.""" import pytest -from ...src.keyring.kms_rsa_keyring_example import ( - kms_rsa_keyring_example, +from ....src.keyring.kms_rsa_keyring_example.utility import ( should_get_new_public_key, write_public_key_pem_for_rsa_key, ) -from ..test_utils import ( +from ....src.keyring.kms_rsa_keyring_example.with_encrypted_table import kms_rsa_keyring_table_example +from ...test_utils import ( TEST_DDB_TABLE_NAME, TEST_KMS_RSA_KEY_ID, ) @@ -16,12 +16,12 @@ pytestmark = [pytest.mark.examples] -def test_kms_rsa_keyring_example(): - """Test the KMS RSA keyring example.""" +def test_kms_rsa_keyring_table_example(): + """Test the KMS RSA keyring table example.""" # You may provide your own RSA public key at EXAMPLE_RSA_PUBLIC_KEY_FILENAME. # This must be the public key for the RSA key represented at rsa_key_arn. # If this file is not present, this will write a UTF-8 encoded PEM file for you. if should_get_new_public_key(): write_public_key_pem_for_rsa_key(TEST_KMS_RSA_KEY_ID) - kms_rsa_keyring_example(TEST_DDB_TABLE_NAME, TEST_KMS_RSA_KEY_ID) + kms_rsa_keyring_table_example(TEST_DDB_TABLE_NAME, TEST_KMS_RSA_KEY_ID) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/mrk_discovery_multi_keyring_example/__init__.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/mrk_discovery_multi_keyring_example/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/mrk_discovery_multi_keyring_example/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/mrk_discovery_multi_keyring_example/test_with_encrypted_client.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/mrk_discovery_multi_keyring_example/test_with_encrypted_client.py new file mode 100644 index 000000000..971f4f1b8 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/mrk_discovery_multi_keyring_example/test_with_encrypted_client.py @@ -0,0 +1,24 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test MRK discovery multi-keyring with encrypted client examples.""" +import pytest + +from ....src.keyring.mrk_discovery_multi_keyring_example.with_encrypted_client import ( + multi_mrk_discovery_keyring_client_example, +) +from ...test_utils import ( + TEST_AWS_ACCOUNT_ID, + TEST_AWS_REGION, + TEST_DDB_TABLE_NAME, + TEST_MRK_KEY_ID, +) + +pytestmark = [pytest.mark.examples] + + +def test_multi_mrk_discovery_keyring_client_example(): + """Test multi_mrk_discovery_keyring_client_example.""" + accounts = [TEST_AWS_ACCOUNT_ID] + regions = [TEST_AWS_REGION] + + multi_mrk_discovery_keyring_client_example(TEST_DDB_TABLE_NAME, TEST_MRK_KEY_ID, accounts, regions) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/mrk_discovery_multi_keyring_example/test_with_encrypted_table.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/mrk_discovery_multi_keyring_example/test_with_encrypted_table.py new file mode 100644 index 000000000..c16489073 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/mrk_discovery_multi_keyring_example/test_with_encrypted_table.py @@ -0,0 +1,24 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test MRK discovery multi-keyring with encrypted table examples.""" +import pytest + +from ....src.keyring.mrk_discovery_multi_keyring_example.with_encrypted_table import ( + multi_mrk_discovery_keyring_table_example, +) +from ...test_utils import ( + TEST_AWS_ACCOUNT_ID, + TEST_AWS_REGION, + TEST_DDB_TABLE_NAME, + TEST_MRK_KEY_ID, +) + +pytestmark = [pytest.mark.examples] + + +def test_multi_mrk_discovery_keyring_table_example(): + """Test multi_mrk_discovery_keyring_table_example.""" + accounts = [TEST_AWS_ACCOUNT_ID] + regions = [TEST_AWS_REGION] + + multi_mrk_discovery_keyring_table_example(TEST_DDB_TABLE_NAME, TEST_MRK_KEY_ID, accounts, regions) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/mrk_multi_keyring_example/__init__.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/mrk_multi_keyring_example/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/mrk_multi_keyring_example/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/mrk_multi_keyring_example/test_with_encrypted_client.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/mrk_multi_keyring_example/test_with_encrypted_client.py new file mode 100644 index 000000000..2459a9c96 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/mrk_multi_keyring_example/test_with_encrypted_client.py @@ -0,0 +1,23 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test MRK multi-keyring with encrypted client examples.""" +import pytest + +from ....src.keyring.mrk_multi_keyring_example.with_encrypted_client import ( + multi_mrk_keyring_client_example, +) +from ...test_utils import ( + TEST_DDB_TABLE_NAME, + TEST_KMS_KEY_ID, + TEST_MRK_KEY_ID, + TEST_MRK_REPLICA_KEY_ID_US_EAST_1, +) + +pytestmark = [pytest.mark.examples] + + +def test_multi_mrk_keyring_client_example(): + """Test multi_mrk_keyring_client_example.""" + multi_mrk_keyring_client_example( + TEST_DDB_TABLE_NAME, TEST_MRK_KEY_ID, TEST_KMS_KEY_ID, TEST_MRK_REPLICA_KEY_ID_US_EAST_1 + ) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_mrk_multi_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/mrk_multi_keyring_example/test_with_encrypted_table.py similarity index 52% rename from Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_mrk_multi_keyring_example.py rename to Examples/runtimes/python/DynamoDBEncryption/test/keyring/mrk_multi_keyring_example/test_with_encrypted_table.py index e63ccc323..25a556943 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_mrk_multi_keyring_example.py +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/mrk_multi_keyring_example/test_with_encrypted_table.py @@ -1,10 +1,12 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -"""Test MRK multi-keyring example.""" +"""Test MRK multi-keyring with encrypted table examples.""" import pytest -from ...src.keyring.mrk_multi_keyring_example import multi_mrk_keyring_get_item_put_item -from ..test_utils import ( +from ....src.keyring.mrk_multi_keyring_example.with_encrypted_table import ( + multi_mrk_keyring_table_example, +) +from ...test_utils import ( TEST_DDB_TABLE_NAME, TEST_KMS_KEY_ID, TEST_MRK_KEY_ID, @@ -14,8 +16,8 @@ pytestmark = [pytest.mark.examples] -def test_mrk_multi_keyring_example(): - """Test mrk_multi_keyring_example.""" - multi_mrk_keyring_get_item_put_item( +def test_multi_mrk_keyring_table_example(): + """Test multi_mrk_keyring_table_example.""" + multi_mrk_keyring_table_example( TEST_DDB_TABLE_NAME, TEST_MRK_KEY_ID, TEST_KMS_KEY_ID, TEST_MRK_REPLICA_KEY_ID_US_EAST_1 ) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/multi_keyring_example/__init__.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/multi_keyring_example/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/multi_keyring_example/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/multi_keyring_example/test_with_encrypted_client.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/multi_keyring_example/test_with_encrypted_client.py new file mode 100644 index 000000000..2bf6a5f2a --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/multi_keyring_example/test_with_encrypted_client.py @@ -0,0 +1,19 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test multi-keyring example with encrypted client.""" +import secrets + +import pytest + +from ....src.keyring.multi_keyring_example.with_encrypted_client import multi_keyring_client_example +from ...test_utils import TEST_DDB_TABLE_NAME, TEST_KMS_KEY_ID + +pytestmark = [pytest.mark.examples] + + +def test_multi_keyring_client_example(): + """Test multi_keyring_client_example.""" + # Generate a new AES key + aes_key_bytes = secrets.token_bytes(32) # 32 bytes = 256 bits + + multi_keyring_client_example(TEST_DDB_TABLE_NAME, TEST_KMS_KEY_ID, aes_key_bytes) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/multi_keyring_example/test_with_encrypted_table.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/multi_keyring_example/test_with_encrypted_table.py new file mode 100644 index 000000000..cec803c27 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/multi_keyring_example/test_with_encrypted_table.py @@ -0,0 +1,19 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test multi-keyring example with encrypted table.""" +import secrets + +import pytest + +from ....src.keyring.multi_keyring_example.with_encrypted_table import multi_keyring_table_example +from ...test_utils import TEST_DDB_TABLE_NAME, TEST_KMS_KEY_ID + +pytestmark = [pytest.mark.examples] + + +def test_multi_keyring_table_example(): + """Test multi_keyring_table_example.""" + # Generate a new AES key + aes_key_bytes = secrets.token_bytes(32) # 32 bytes = 256 bits + + multi_keyring_table_example(TEST_DDB_TABLE_NAME, TEST_KMS_KEY_ID, aes_key_bytes) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/raw_aes_keyring_example/__init__.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/raw_aes_keyring_example/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/raw_aes_keyring_example/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/raw_aes_keyring_example/test_with_encrypted_client.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/raw_aes_keyring_example/test_with_encrypted_client.py new file mode 100644 index 000000000..8d81b2288 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/raw_aes_keyring_example/test_with_encrypted_client.py @@ -0,0 +1,19 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test raw AES keyring with encrypted client example.""" +import secrets + +import pytest + +from ....src.keyring.raw_aes_keyring_example.with_encrypted_client import raw_aes_keyring_client_example +from ...test_utils import TEST_DDB_TABLE_NAME + +pytestmark = [pytest.mark.examples] + + +def test_raw_aes_keyring_client_example(): + """Test raw_aes_keyring_client_example.""" + # Generate a new AES key + aes_key_bytes = secrets.token_bytes(32) # 32 bytes = 256 bits + + raw_aes_keyring_client_example(TEST_DDB_TABLE_NAME, aes_key_bytes) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/raw_aes_keyring_example/test_with_encrypted_table.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/raw_aes_keyring_example/test_with_encrypted_table.py new file mode 100644 index 000000000..ef5a7f8ca --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/raw_aes_keyring_example/test_with_encrypted_table.py @@ -0,0 +1,19 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test raw AES keyring with encrypted table example.""" +import secrets + +import pytest + +from ....src.keyring.raw_aes_keyring_example.with_encrypted_table import raw_aes_keyring_table_example +from ...test_utils import TEST_DDB_TABLE_NAME + +pytestmark = [pytest.mark.examples] + + +def test_raw_aes_keyring_table_example(): + """Test raw_aes_keyring_table_example.""" + # Generate a new AES key + aes_key_bytes = secrets.token_bytes(32) # 32 bytes = 256 bits + + raw_aes_keyring_table_example(TEST_DDB_TABLE_NAME, aes_key_bytes) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/raw_ecdh_keyring_example/__init__.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/raw_ecdh_keyring_example/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/raw_ecdh_keyring_example/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_raw_ecdh_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/raw_ecdh_keyring_example/test_with_encrypted_client.py similarity index 60% rename from Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_raw_ecdh_keyring_example.py rename to Examples/runtimes/python/DynamoDBEncryption/test/keyring/raw_ecdh_keyring_example/test_with_encrypted_client.py index 6a7676d89..1b79e4fd6 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_raw_ecdh_keyring_example.py +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/raw_ecdh_keyring_example/test_with_encrypted_client.py @@ -1,23 +1,25 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -"""Test raw ECDH keyring examples.""" +"""Test raw ECDH keyring with encrypted client example.""" import pytest from aws_cryptography_primitives.smithygenerated.aws_cryptography_primitives.models import ECDHCurveSpec -from ...src.keyring.raw_ecdh_keyring_example import ( - discovery_raw_ecdh_keyring_get_item, - ephemeral_raw_ecdh_keyring_put_item, +from ....src.keyring.raw_ecdh_keyring_example.utility import ( generate_ecc_key_pairs, - raw_ecdh_keyring_get_item_put_item, should_generate_new_ecc_key_pairs, ) -from ..test_utils import TEST_DDB_TABLE_NAME +from ....src.keyring.raw_ecdh_keyring_example.with_encrypted_client import ( + discovery_raw_ecdh_keyring_get_item_with_ecnrypted_client, + ephemeral_raw_ecdh_keyring_put_item_with_ecnrypted_client, + raw_ecdh_keyring_get_item_put_item_with_ecnrypted_client, +) +from ...test_utils import TEST_DDB_TABLE_NAME pytestmark = [pytest.mark.examples] -def test_static_raw_ecdh_keyring_example(): - """Test raw_ecdh_keyring_example with static configuration.""" +def test_raw_ecdh_keyring_get_item_put_item(): + """Test raw_ecdh_keyring_get_item_put_item with static configuration.""" # You may provide your own ECC Key pairs in the files located at # - EXAMPLE_ECC_PRIVATE_KEY_FILENAME_SENDER # - EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT @@ -29,11 +31,11 @@ def test_static_raw_ecdh_keyring_example(): # Part of using these keyrings is knowing which curve the keys used in the key agreement # lie on. The keyring will fail if the keys do not lie on the configured curve. - raw_ecdh_keyring_get_item_put_item(TEST_DDB_TABLE_NAME, ECDHCurveSpec.ECC_NIST_P256) + raw_ecdh_keyring_get_item_put_item_with_ecnrypted_client(TEST_DDB_TABLE_NAME, ECDHCurveSpec.ECC_NIST_P256) -def test_ephemeral_raw_ecdh_keyring_example(): - """Test raw_ecdh_keyring_example with ephemeral configuration.""" +def test_ephemeral_raw_ecdh_keyring_put_item(): + """Test ephemeral_raw_ecdh_keyring_put_item with ephemeral configuration.""" # You may provide your own ECC Public Key in the files located at # - EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT # If you provide this, the keys MUST be on curve P256 @@ -44,11 +46,11 @@ def test_ephemeral_raw_ecdh_keyring_example(): # Part of using these keyrings is knowing which curve the keys used in the key agreement # lie on. The keyring will fail if the keys do not lie on the configured curve. - ephemeral_raw_ecdh_keyring_put_item(TEST_DDB_TABLE_NAME, ECDHCurveSpec.ECC_NIST_P256) + ephemeral_raw_ecdh_keyring_put_item_with_ecnrypted_client(TEST_DDB_TABLE_NAME, ECDHCurveSpec.ECC_NIST_P256) -def test_discovery_raw_ecdh_keyring_example(): - """Test raw_ecdh_keyring_example with discovery configuration.""" +def test_discovery_raw_ecdh_keyring_get_item(): + """Test discovery_raw_ecdh_keyring_get_item with discovery configuration.""" # You may provide your own ECC Public Key in the files located at # - EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT # - EXAMPLE_ECC_PRIVATE_KEY_FILENAME_RECIPIENT @@ -59,16 +61,16 @@ def test_discovery_raw_ecdh_keyring_example(): generate_ecc_key_pairs() # The discovery configuration is not allowed to encrypt - # To understand this example best, we will write a record with the ephemeral configuration - # in the previous example. This means that the recipient public key configured on - # both keyrings is the same. This means that the other party has the recipient public key - # and is writing messages meant only for the owner of the recipient public key to decrypt. + # To understand this example best, first, write a record with the ephemeral configuration + # This means that the recipient public key configured on both keyrings is the same. + # The other party has the recipient public key and is writing messages meant only for + # the owner of the recipient public key to decrypt. # In this call we are writing a record that is written with an ephemeral sender key pair. # The recipient will be able to decrypt the message - ephemeral_raw_ecdh_keyring_put_item(TEST_DDB_TABLE_NAME, ECDHCurveSpec.ECC_NIST_P256) + ephemeral_raw_ecdh_keyring_put_item_with_ecnrypted_client(TEST_DDB_TABLE_NAME, ECDHCurveSpec.ECC_NIST_P256) # In this call we are reading a record that was written with the recipient's public key. # It will use the recipient's private key and the sender's public key stored in the message to # calculate the appropriate shared secret to successfully decrypt the message. - discovery_raw_ecdh_keyring_get_item(TEST_DDB_TABLE_NAME, ECDHCurveSpec.ECC_NIST_P256) + discovery_raw_ecdh_keyring_get_item_with_ecnrypted_client(TEST_DDB_TABLE_NAME, ECDHCurveSpec.ECC_NIST_P256) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/raw_ecdh_keyring_example/test_with_encrypted_table.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/raw_ecdh_keyring_example/test_with_encrypted_table.py new file mode 100644 index 000000000..2695931f5 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/raw_ecdh_keyring_example/test_with_encrypted_table.py @@ -0,0 +1,76 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test raw ECDH keyring with encrypted table example.""" +import pytest +from aws_cryptography_primitives.smithygenerated.aws_cryptography_primitives.models import ECDHCurveSpec + +from ....src.keyring.raw_ecdh_keyring_example.utility import ( + generate_ecc_key_pairs, + should_generate_new_ecc_key_pairs, +) +from ....src.keyring.raw_ecdh_keyring_example.with_encrypted_table import ( + discovery_raw_ecdh_keyring_get_item_with_ecnrypted_table, + ephemeral_raw_ecdh_keyring_put_item_with_ecnrypted_table, + raw_ecdh_keyring_get_item_put_item_with_ecnrypted_table, +) +from ...test_utils import TEST_DDB_TABLE_NAME + +pytestmark = [pytest.mark.examples] + + +def test_raw_ecdh_keyring_get_item_put_item_with_table(): + """Test raw_ecdh_keyring_get_item_put_item with static configuration using EncryptedTable.""" + # You may provide your own ECC Key pairs in the files located at + # - EXAMPLE_ECC_PRIVATE_KEY_FILENAME_SENDER + # - EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT + # If you provide this, the keys MUST be on curve P256 + # If these files are not present, this will generate a pair for you. + # For this example we will use the curve P256. + if should_generate_new_ecc_key_pairs(): + generate_ecc_key_pairs() + + # Part of using these keyrings is knowing which curve the keys used in the key agreement + # lie on. The keyring will fail if the keys do not lie on the configured curve. + raw_ecdh_keyring_get_item_put_item_with_ecnrypted_table(TEST_DDB_TABLE_NAME, ECDHCurveSpec.ECC_NIST_P256) + + +def test_ephemeral_raw_ecdh_keyring_put_item_with_table(): + """Test ephemeral_raw_ecdh_keyring_put_item with ephemeral configuration using EncryptedTable.""" + # You may provide your own ECC Public Key in the files located at + # - EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT + # If you provide this, the keys MUST be on curve P256 + # If these files are not present, this will generate a pair for you. + # For this example we will use the curve P256. + if should_generate_new_ecc_key_pairs(): + generate_ecc_key_pairs() + + # Part of using these keyrings is knowing which curve the keys used in the key agreement + # lie on. The keyring will fail if the keys do not lie on the configured curve. + ephemeral_raw_ecdh_keyring_put_item_with_ecnrypted_table(TEST_DDB_TABLE_NAME, ECDHCurveSpec.ECC_NIST_P256) + + +def test_discovery_raw_ecdh_keyring_get_item_with_table(): + """Test discovery_raw_ecdh_keyring_get_item with discovery configuration using EncryptedTable.""" + # You may provide your own ECC Public Key in the files located at + # - EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT + # - EXAMPLE_ECC_PRIVATE_KEY_FILENAME_RECIPIENT + # If you provide this, the keys MUST be on curve P256 + # If these files are not present, this will generate a pair for you. + # For this example we will use the curve P256. + if should_generate_new_ecc_key_pairs(): + generate_ecc_key_pairs() + + # The discovery configuration is not allowed to encrypt + # To understand this example best, first, write a record with the ephemeral configuration + # This means that the recipient public key configured on both keyrings is the same. + # The other party has the recipient public key and is writing messages meant only for + # the owner of the recipient public key to decrypt. + + # In this call we are writing a record that is written with an ephemeral sender key pair. + # The recipient will be able to decrypt the message + ephemeral_raw_ecdh_keyring_put_item_with_ecnrypted_table(TEST_DDB_TABLE_NAME, ECDHCurveSpec.ECC_NIST_P256) + + # In this call we are reading a record that was written with the recipient's public key. + # It will use the recipient's private key and the sender's public key stored in the message to + # calculate the appropriate shared secret to successfully decrypt the message. + discovery_raw_ecdh_keyring_get_item_with_ecnrypted_table(TEST_DDB_TABLE_NAME, ECDHCurveSpec.ECC_NIST_P256) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/raw_rsa_keyring_example/__init__.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/raw_rsa_keyring_example/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/raw_rsa_keyring_example/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/raw_rsa_keyring_example/test_with_encrypted_client.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/raw_rsa_keyring_example/test_with_encrypted_client.py new file mode 100644 index 000000000..34152129e --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/raw_rsa_keyring_example/test_with_encrypted_client.py @@ -0,0 +1,29 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test raw RSA keyring with encrypted client example.""" +import pytest + +from ....src.keyring.raw_rsa_keyring_example.utility import ( + EXAMPLE_RSA_PRIVATE_KEY_FILENAME, + EXAMPLE_RSA_PUBLIC_KEY_FILENAME, + generate_rsa_key_pair, + should_generate_new_rsa_key_pair, +) +from ....src.keyring.raw_rsa_keyring_example.with_encrypted_client import raw_rsa_keyring_client_example +from ...test_utils import TEST_DDB_TABLE_NAME + +pytestmark = [pytest.mark.examples] + + +def test_raw_rsa_keyring_client_example(): + """Test raw_rsa_keyring_client_example.""" + # You may provide your own RSA key pair in the files located at + # - EXAMPLE_RSA_PRIVATE_KEY_FILENAME + # - EXAMPLE_RSA_PUBLIC_KEY_FILENAME + # If these files are not present, this will generate a pair for you + if should_generate_new_rsa_key_pair(): + generate_rsa_key_pair() + + raw_rsa_keyring_client_example( + TEST_DDB_TABLE_NAME, EXAMPLE_RSA_PRIVATE_KEY_FILENAME, EXAMPLE_RSA_PUBLIC_KEY_FILENAME + ) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/raw_rsa_keyring_example/test_with_encrypted_table.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/raw_rsa_keyring_example/test_with_encrypted_table.py new file mode 100644 index 000000000..0358fbb6d --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/raw_rsa_keyring_example/test_with_encrypted_table.py @@ -0,0 +1,29 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test raw RSA keyring with encrypted table example.""" +import pytest + +from ....src.keyring.raw_rsa_keyring_example.utility import ( + EXAMPLE_RSA_PRIVATE_KEY_FILENAME, + EXAMPLE_RSA_PUBLIC_KEY_FILENAME, + generate_rsa_key_pair, + should_generate_new_rsa_key_pair, +) +from ....src.keyring.raw_rsa_keyring_example.with_encrypted_table import raw_rsa_keyring_table_example +from ...test_utils import TEST_DDB_TABLE_NAME + +pytestmark = [pytest.mark.examples] + + +def test_raw_rsa_keyring_table_example(): + """Test raw_rsa_keyring_table_example.""" + # You may provide your own RSA key pair in the files located at + # - EXAMPLE_RSA_PRIVATE_KEY_FILENAME + # - EXAMPLE_RSA_PUBLIC_KEY_FILENAME + # If these files are not present, this will generate a pair for you + if should_generate_new_rsa_key_pair(): + generate_rsa_key_pair() + + raw_rsa_keyring_table_example( + TEST_DDB_TABLE_NAME, EXAMPLE_RSA_PRIVATE_KEY_FILENAME, EXAMPLE_RSA_PUBLIC_KEY_FILENAME + ) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/shared_cache_across_hierarchical_keyrings_example/__init__.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/shared_cache_across_hierarchical_keyrings_example/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/shared_cache_across_hierarchical_keyrings_example/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/shared_cache_across_hierarchical_keyrings_example/test_with_encrypted_client.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/shared_cache_across_hierarchical_keyrings_example/test_with_encrypted_client.py new file mode 100644 index 000000000..2b63dbb52 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/shared_cache_across_hierarchical_keyrings_example/test_with_encrypted_client.py @@ -0,0 +1,43 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test for the shared cache across hierarchical keyrings example.""" +import time + +import pytest + +from ....src.create_keystore_key_example import keystore_create_key +from ....src.keyring.shared_cache_across_hierarchical_keyrings_example.with_encrypted_client import ( + shared_cache_across_hierarchical_keyrings_example, +) +from ...cleanup import delete_branch_key +from ...test_utils import ( + TEST_DDB_TABLE_NAME, + TEST_KEYSTORE_KMS_KEY_ID, + TEST_KEYSTORE_NAME, + TEST_LOGICAL_KEYSTORE_NAME, + TEST_PARTITION_ID, +) + +pytestmark = [pytest.mark.examples] + + +def test_shared_cache_across_hierarchical_keyrings_example(): + """Test the shared cache across hierarchical keyrings example.""" + # Create new branch key for test + key_id = keystore_create_key(TEST_KEYSTORE_NAME, TEST_LOGICAL_KEYSTORE_NAME, TEST_KEYSTORE_KMS_KEY_ID) + + # Key creation is eventually consistent, so wait 5 seconds to decrease the likelihood + # our test fails due to eventual consistency issues. + time.sleep(5) + + shared_cache_across_hierarchical_keyrings_example( + TEST_DDB_TABLE_NAME, + key_id, + TEST_KEYSTORE_NAME, + TEST_LOGICAL_KEYSTORE_NAME, + TEST_PARTITION_ID, + TEST_KEYSTORE_KMS_KEY_ID, + ) + + # Cleanup Branch Key + delete_branch_key(key_id, TEST_KEYSTORE_NAME, None) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_shared_cache_across_hierarchical_keyrings_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/shared_cache_across_hierarchical_keyrings_example/test_with_encrypted_table.py similarity index 83% rename from Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_shared_cache_across_hierarchical_keyrings_example.py rename to Examples/runtimes/python/DynamoDBEncryption/test/keyring/shared_cache_across_hierarchical_keyrings_example/test_with_encrypted_table.py index 2cfc10fe7..862deed82 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_shared_cache_across_hierarchical_keyrings_example.py +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/shared_cache_across_hierarchical_keyrings_example/test_with_encrypted_table.py @@ -5,12 +5,12 @@ import pytest -from ...src.create_keystore_key_example import keystore_create_key -from ...src.keyring.shared_cache_across_hierarchical_keyrings_example import ( +from ....src.create_keystore_key_example import keystore_create_key +from ....src.keyring.shared_cache_across_hierarchical_keyrings_example.with_encrypted_table import ( shared_cache_across_hierarchical_keyrings_example, ) -from ..cleanup import delete_branch_key -from ..test_utils import ( +from ...cleanup import delete_branch_key +from ...test_utils import ( TEST_DDB_TABLE_NAME, TEST_KEYSTORE_KMS_KEY_ID, TEST_KEYSTORE_NAME, diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_hierarchical_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_hierarchical_keyring_example.py deleted file mode 100644 index 61cdb10a0..000000000 --- a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_hierarchical_keyring_example.py +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: Apache-2.0 -"""Test hierarchical keyring example.""" -import time - -import pytest - -from ...src.create_keystore_key_example import keystore_create_key -from ...src.keyring.hierarchical_keyring_example import hierarchical_keyring_get_item_put_item -from ..cleanup import delete_branch_key -from ..test_utils import ( - TEST_DDB_TABLE_NAME, - TEST_KEYSTORE_KMS_KEY_ID, - TEST_KEYSTORE_NAME, - TEST_LOGICAL_KEYSTORE_NAME, -) - -pytestmark = [pytest.mark.examples] - - -def test_hierarchical_keyring_example(): - """Test hierarchical_keyring_example.""" - # Create new branch keys for test - key_id1 = keystore_create_key(TEST_KEYSTORE_NAME, TEST_LOGICAL_KEYSTORE_NAME, TEST_KEYSTORE_KMS_KEY_ID) - key_id2 = keystore_create_key(TEST_KEYSTORE_NAME, TEST_LOGICAL_KEYSTORE_NAME, TEST_KEYSTORE_KMS_KEY_ID) - - # Key creation is eventually consistent, so wait 5 seconds to decrease the likelihood - # our test fails due to eventual consistency issues. - time.sleep(5) - - hierarchical_keyring_get_item_put_item( - TEST_DDB_TABLE_NAME, key_id1, key_id2, TEST_KEYSTORE_NAME, TEST_LOGICAL_KEYSTORE_NAME, TEST_KEYSTORE_KMS_KEY_ID - ) - - # Cleanup Branch Key - delete_branch_key(key_id1, TEST_KEYSTORE_NAME, None) - delete_branch_key(key_id2, TEST_KEYSTORE_NAME, None) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_kms_ecdh_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_kms_ecdh_keyring_example.py deleted file mode 100644 index d54bd273e..000000000 --- a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_kms_ecdh_keyring_example.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: Apache-2.0 -"""Test KMS ECDH keyring examples.""" -import pytest - -from ...src.keyring.kms_ecdh_keyring_example import ( - EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME, - EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME, - kms_ecdh_discovery_get_item, - kms_ecdh_keyring_get_item_put_item, - should_get_new_public_keys, - write_public_key_pem_for_ecc_key, -) -from ..test_utils import TEST_DDB_TABLE_NAME, TEST_KMS_ECDH_KEY_ID_P256_RECIPIENT, TEST_KMS_ECDH_KEY_ID_P256_SENDER - -pytestmark = [pytest.mark.examples] - - -def test_kms_ecdh_keyring_example_static(): - """Test kms_ecdh_keyring_example with static configuration.""" - # You may provide your own ECC public keys at - # - EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME - # - EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME. - # If you provide these, the keys MUST be on curve P256 - # This must be the public key for the ECC key represented at eccKeyArn - # If this file is not present, this will write a UTF-8 encoded PEM file for you. - if should_get_new_public_keys(): - write_public_key_pem_for_ecc_key(TEST_KMS_ECDH_KEY_ID_P256_SENDER, EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME) - write_public_key_pem_for_ecc_key(TEST_KMS_ECDH_KEY_ID_P256_RECIPIENT, EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME) - - kms_ecdh_keyring_get_item_put_item(TEST_DDB_TABLE_NAME, TEST_KMS_ECDH_KEY_ID_P256_SENDER) - - -def test_kms_ecdh_keyring_example_discovery(): - """Test kms_ecdh_keyring_example with discovery configuration.""" - # In this example you do not need to provide the recipient ECC Public Key. - # On initialization, the keyring will call KMS:getPublicKey on the configured - # recipientKmsIdentifier set on the keyring. This example uses the previous example - # to write an item meant for the recipient. - kms_ecdh_discovery_get_item(TEST_DDB_TABLE_NAME, TEST_KMS_ECDH_KEY_ID_P256_RECIPIENT) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_mrk_discovery_multi_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_mrk_discovery_multi_keyring_example.py deleted file mode 100644 index d2238bfa7..000000000 --- a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_mrk_discovery_multi_keyring_example.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: Apache-2.0 -"""Test MRK discovery multi-keyring example.""" -import pytest - -from ...src.keyring.mrk_discovery_multi_keyring_example import multi_mrk_discovery_keyring_get_item_put_item -from ..test_utils import ( - TEST_AWS_ACCOUNT_ID, - TEST_AWS_REGION, - TEST_DDB_TABLE_NAME, - TEST_MRK_KEY_ID, -) - -pytestmark = [pytest.mark.examples] - - -def test_mrk_discovery_multi_keyring_example(): - """Test mrk_discovery_multi_keyring_example.""" - accounts = [TEST_AWS_ACCOUNT_ID] - regions = [TEST_AWS_REGION] - - multi_mrk_discovery_keyring_get_item_put_item(TEST_DDB_TABLE_NAME, TEST_MRK_KEY_ID, accounts, regions) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_multi_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_multi_keyring_example.py deleted file mode 100644 index 0db6f3d80..000000000 --- a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_multi_keyring_example.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: Apache-2.0 -"""Test multi-keyring example.""" -import secrets - -import pytest - -from ...src.keyring.multi_keyring_example import multi_keyring_get_item_put_item -from ..test_utils import TEST_DDB_TABLE_NAME, TEST_KMS_KEY_ID - -pytestmark = [pytest.mark.examples] - - -def test_multi_keyring_example(): - """Test multi_keyring_example.""" - # Generate a new AES key - aes_key_bytes = secrets.token_bytes(32) # 32 bytes = 256 bits - - multi_keyring_get_item_put_item(TEST_DDB_TABLE_NAME, TEST_KMS_KEY_ID, aes_key_bytes) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_raw_aes_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_raw_aes_keyring_example.py deleted file mode 100644 index 93da5bddf..000000000 --- a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_raw_aes_keyring_example.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: Apache-2.0 -"""Test raw AES keyring example.""" -import secrets - -import pytest - -from ...src.keyring.raw_aes_keyring_example import raw_aes_keyring_get_item_put_item -from ..test_utils import TEST_DDB_TABLE_NAME - -pytestmark = [pytest.mark.examples] - - -def test_raw_aes_keyring_example(): - """Test raw_aes_keyring_example.""" - # Generate a new AES key - aes_key_bytes = secrets.token_bytes(32) # 32 bytes = 256 bits - - raw_aes_keyring_get_item_put_item(TEST_DDB_TABLE_NAME, aes_key_bytes) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_raw_rsa_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_raw_rsa_keyring_example.py deleted file mode 100644 index 590a7948e..000000000 --- a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_raw_rsa_keyring_example.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: Apache-2.0 -"""Test for the Raw RSA keyring example.""" -import pytest - -from ...src.keyring.raw_rsa_keyring_example import ( - generate_rsa_key_pair, - raw_rsa_keyring_example, - should_generate_new_rsa_key_pair, -) -from ..test_utils import TEST_DDB_TABLE_NAME - -pytestmark = [pytest.mark.examples] - - -def test_raw_rsa_keyring_example(): - """Test the Raw RSA keyring example.""" - # You may provide your own RSA key pair in the files located at - # - EXAMPLE_RSA_PRIVATE_KEY_FILENAME - # - EXAMPLE_RSA_PUBLIC_KEY_FILENAME - # If these files are not present, this will generate a pair for you - if should_generate_new_rsa_key_pair(): - generate_rsa_key_pair() - - raw_rsa_keyring_example(TEST_DDB_TABLE_NAME) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/scan_error_example/__init__.py b/Examples/runtimes/python/DynamoDBEncryption/test/scan_error_example/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/scan_error_example/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/scan_error_example/test_with_encrypted_client.py b/Examples/runtimes/python/DynamoDBEncryption/test/scan_error_example/test_with_encrypted_client.py new file mode 100644 index 000000000..2791cfc36 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/scan_error_example/test_with_encrypted_client.py @@ -0,0 +1,13 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test scan error example.""" +import pytest +from ...src.scan_error_example.with_encrypted_client import scan_error_with_client +from ..test_utils import TEST_DDB_TABLE_NAME, TEST_KMS_KEY_ID + +pytestmark = [pytest.mark.examples] + + +def test_scan_error(): + """Test scan_error.""" + scan_error_with_client(TEST_KMS_KEY_ID, TEST_DDB_TABLE_NAME) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/test_scan_error_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/scan_error_example/test_with_encrypted_table.py similarity index 53% rename from Examples/runtimes/python/DynamoDBEncryption/test/test_scan_error_example.py rename to Examples/runtimes/python/DynamoDBEncryption/test/scan_error_example/test_with_encrypted_table.py index ee0c3710d..a448bb02c 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/test/test_scan_error_example.py +++ b/Examples/runtimes/python/DynamoDBEncryption/test/scan_error_example/test_with_encrypted_table.py @@ -2,13 +2,12 @@ # SPDX-License-Identifier: Apache-2.0 """Test scan error example.""" import pytest - -from ..src.scan_error_example import scan_error -from .test_utils import TEST_DDB_TABLE_NAME, TEST_KMS_KEY_ID +from ...src.scan_error_example.with_encrypted_table import scan_error_with_table +from ..test_utils import TEST_DDB_TABLE_NAME, TEST_KMS_KEY_ID pytestmark = [pytest.mark.examples] def test_scan_error(): """Test scan_error.""" - scan_error(TEST_KMS_KEY_ID, TEST_DDB_TABLE_NAME) + scan_error_with_table(TEST_KMS_KEY_ID, TEST_DDB_TABLE_NAME) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/basic_searchable_encryption_example/__init__.py b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/basic_searchable_encryption_example/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/basic_searchable_encryption_example/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/basic_searchable_encryption_example/test_with_with_encrypted_client.py b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/basic_searchable_encryption_example/test_with_with_encrypted_client.py new file mode 100644 index 000000000..c8ba23769 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/basic_searchable_encryption_example/test_with_with_encrypted_client.py @@ -0,0 +1,42 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test basic searchable encryption example.""" +import time + +import pytest + +from ....src.create_keystore_key_example import keystore_create_key +from ....src.searchable_encryption.basic_searchable_encryption_example.with_encrypted_client import ( + basic_searchable_encryption_client_example +) +from ...cleanup import delete_branch_key +from ..searchable_encryption_test_utils import ( + TEST_BRANCH_KEY_WRAPPING_KMS_KEY_ARN, + TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME, + TEST_LOGICAL_KEYSTORE_NAME, + UNIT_INSPECTION_TEST_DDB_TABLE_NAME, +) + +pytestmark = [pytest.mark.examples] + + +def test_basic_searchable_encryption_client_example(): + """Test basic searchable encryption example.""" + # Create new branch key for test + key_id = keystore_create_key( + TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME, TEST_LOGICAL_KEYSTORE_NAME, TEST_BRANCH_KEY_WRAPPING_KMS_KEY_ARN + ) + + # Key creation is eventually consistent, so wait 5 seconds to decrease the likelihood + # our test fails due to eventual consistency issues. + time.sleep(5) + + basic_searchable_encryption_client_example( + UNIT_INSPECTION_TEST_DDB_TABLE_NAME, + key_id, + TEST_BRANCH_KEY_WRAPPING_KMS_KEY_ARN, + TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME, + ) + + # Cleanup Branch Key + delete_branch_key(key_id, TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME, None) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/test_basic_searchable_encryption_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/basic_searchable_encryption_example/test_with_with_encrypted_table.py similarity index 71% rename from Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/test_basic_searchable_encryption_example.py rename to Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/basic_searchable_encryption_example/test_with_with_encrypted_table.py index 0292c26c2..b811d67c6 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/test_basic_searchable_encryption_example.py +++ b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/basic_searchable_encryption_example/test_with_with_encrypted_table.py @@ -5,10 +5,12 @@ import pytest -from ...src.create_keystore_key_example import keystore_create_key -from ...src.searchable_encryption.basic_searchable_encryption_example import put_item_query_item_with_beacon -from ..cleanup import delete_branch_key -from .searchable_encryption_test_utils import ( +from ....src.create_keystore_key_example import keystore_create_key +from ....src.searchable_encryption.basic_searchable_encryption_example.with_encrypted_table import ( + basic_searchable_encryption_table_example +) +from ...cleanup import delete_branch_key +from ..searchable_encryption_test_utils import ( TEST_BRANCH_KEY_WRAPPING_KMS_KEY_ARN, TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME, TEST_LOGICAL_KEYSTORE_NAME, @@ -18,7 +20,7 @@ pytestmark = [pytest.mark.examples] -def test_basic_example(): +def test_basic_searchable_encryption_table_example(): """Test basic searchable encryption example.""" # Create new branch key for test key_id = keystore_create_key( @@ -29,7 +31,7 @@ def test_basic_example(): # our test fails due to eventual consistency issues. time.sleep(5) - put_item_query_item_with_beacon( + basic_searchable_encryption_table_example( UNIT_INSPECTION_TEST_DDB_TABLE_NAME, key_id, TEST_BRANCH_KEY_WRAPPING_KMS_KEY_ARN, diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/beacon_styles_searchable_encryption_example/__init__.py b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/beacon_styles_searchable_encryption_example/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/beacon_styles_searchable_encryption_example/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/test_beacon_styles_searchable_encryption_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/beacon_styles_searchable_encryption_example/test_with_with_encrypted_client.py similarity index 73% rename from Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/test_beacon_styles_searchable_encryption_example.py rename to Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/beacon_styles_searchable_encryption_example/test_with_with_encrypted_client.py index 65e7f929b..41823c622 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/test_beacon_styles_searchable_encryption_example.py +++ b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/beacon_styles_searchable_encryption_example/test_with_with_encrypted_client.py @@ -5,12 +5,12 @@ import pytest -from ...src.create_keystore_key_example import keystore_create_key -from ...src.searchable_encryption.beacon_styles_searchable_encryption_example import ( - put_item_query_item_with_beacon_styles, +from ....src.create_keystore_key_example import keystore_create_key +from ....src.searchable_encryption.beacon_styles_searchable_encryption_example.with_encrypted_client import ( + beacon_styles_client_example, ) -from ..cleanup import delete_branch_key -from .searchable_encryption_test_utils import ( +from ...cleanup import delete_branch_key +from ..searchable_encryption_test_utils import ( TEST_BRANCH_KEY_WRAPPING_KMS_KEY_ARN, TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME, TEST_LOGICAL_KEYSTORE_NAME, @@ -20,7 +20,7 @@ pytestmark = [pytest.mark.examples] -def test_beacon_styles_item_encrypt_decrypt(): +def test_beacon_styles_client_example(): """Test beacon styles searchable encryption example.""" # Create new branch key for test key_id = keystore_create_key( @@ -31,7 +31,7 @@ def test_beacon_styles_item_encrypt_decrypt(): # our test fails due to eventual consistency issues. time.sleep(5) - put_item_query_item_with_beacon_styles( + beacon_styles_client_example( UNIT_INSPECTION_TEST_DDB_TABLE_NAME, key_id, TEST_BRANCH_KEY_WRAPPING_KMS_KEY_ARN, diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/beacon_styles_searchable_encryption_example/test_with_with_encrypted_table.py b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/beacon_styles_searchable_encryption_example/test_with_with_encrypted_table.py new file mode 100644 index 000000000..a5df96cf3 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/beacon_styles_searchable_encryption_example/test_with_with_encrypted_table.py @@ -0,0 +1,42 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test beacon styles searchable encryption example.""" +import time + +import pytest + +from ....src.create_keystore_key_example import keystore_create_key +from ....src.searchable_encryption.beacon_styles_searchable_encryption_example.with_encrypted_table import ( + beacon_styles_table_example, +) +from ...cleanup import delete_branch_key +from ..searchable_encryption_test_utils import ( + TEST_BRANCH_KEY_WRAPPING_KMS_KEY_ARN, + TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME, + TEST_LOGICAL_KEYSTORE_NAME, + UNIT_INSPECTION_TEST_DDB_TABLE_NAME, +) + +pytestmark = [pytest.mark.examples] + + +def test_beacon_styles_table_example(): + """Test beacon styles searchable encryption example.""" + # Create new branch key for test + key_id = keystore_create_key( + TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME, TEST_LOGICAL_KEYSTORE_NAME, TEST_BRANCH_KEY_WRAPPING_KMS_KEY_ARN + ) + + # Key creation is eventually consistent, so wait 5 seconds to decrease the likelihood + # our test fails due to eventual consistency issues. + time.sleep(5) + + beacon_styles_table_example( + UNIT_INSPECTION_TEST_DDB_TABLE_NAME, + key_id, + TEST_BRANCH_KEY_WRAPPING_KMS_KEY_ARN, + TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME, + ) + + # Cleanup Branch Key + delete_branch_key(key_id, TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME, None) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/compound_beacon_searchable_encryption_example/__init__.py b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/compound_beacon_searchable_encryption_example/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/compound_beacon_searchable_encryption_example/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/compound_beacon_searchable_encryption_example/test_with_with_encrypted_client.py b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/compound_beacon_searchable_encryption_example/test_with_with_encrypted_client.py new file mode 100644 index 000000000..8453b630e --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/compound_beacon_searchable_encryption_example/test_with_with_encrypted_client.py @@ -0,0 +1,42 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test compound beacon searchable encryption example.""" +import time + +import pytest + +from ....src.create_keystore_key_example import keystore_create_key +from ....src.searchable_encryption.compound_beacon_searchable_encryption_example.with_encrypted_client import ( + compound_beacon_client_example +) +from ...cleanup import delete_branch_key +from ..searchable_encryption_test_utils import ( + TEST_BRANCH_KEY_WRAPPING_KMS_KEY_ARN, + TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME, + TEST_LOGICAL_KEYSTORE_NAME, + UNIT_INSPECTION_TEST_DDB_TABLE_NAME, +) + +pytestmark = [pytest.mark.examples] + + +def test_compound_beacon_client_example(): + """Test compound beacon searchable encryption example.""" + # Create new branch key for test + key_id = keystore_create_key( + TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME, TEST_LOGICAL_KEYSTORE_NAME, TEST_BRANCH_KEY_WRAPPING_KMS_KEY_ARN + ) + + # Key creation is eventually consistent, so wait 5 seconds to decrease the likelihood + # our test fails due to eventual consistency issues. + time.sleep(5) + + compound_beacon_client_example( + UNIT_INSPECTION_TEST_DDB_TABLE_NAME, + key_id, + TEST_BRANCH_KEY_WRAPPING_KMS_KEY_ARN, + TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME, + ) + + # Cleanup Branch Key + delete_branch_key(key_id, TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME, None) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/test_compound_beacon_searchable_encryption_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/compound_beacon_searchable_encryption_example/test_with_with_encrypted_table.py similarity index 73% rename from Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/test_compound_beacon_searchable_encryption_example.py rename to Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/compound_beacon_searchable_encryption_example/test_with_with_encrypted_table.py index 6d05fbf75..ffbfa4b41 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/test_compound_beacon_searchable_encryption_example.py +++ b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/compound_beacon_searchable_encryption_example/test_with_with_encrypted_table.py @@ -5,12 +5,12 @@ import pytest -from ...src.create_keystore_key_example import keystore_create_key -from ...src.searchable_encryption.compound_beacon_searchable_encryption_example import ( - put_item_query_item_with_compound_beacon, +from ....src.create_keystore_key_example import keystore_create_key +from ....src.searchable_encryption.compound_beacon_searchable_encryption_example.with_encrypted_table import ( + compound_beacon_table_example ) -from ..cleanup import delete_branch_key -from .searchable_encryption_test_utils import ( +from ...cleanup import delete_branch_key +from ..searchable_encryption_test_utils import ( TEST_BRANCH_KEY_WRAPPING_KMS_KEY_ARN, TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME, TEST_LOGICAL_KEYSTORE_NAME, @@ -20,7 +20,7 @@ pytestmark = [pytest.mark.examples] -def test_compound_item_encrypt_decrypt(): +def test_compound_beacon_table_example(): """Test compound beacon searchable encryption example.""" # Create new branch key for test key_id = keystore_create_key( @@ -31,7 +31,7 @@ def test_compound_item_encrypt_decrypt(): # our test fails due to eventual consistency issues. time.sleep(5) - put_item_query_item_with_compound_beacon( + compound_beacon_table_example( UNIT_INSPECTION_TEST_DDB_TABLE_NAME, key_id, TEST_BRANCH_KEY_WRAPPING_KMS_KEY_ARN, diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/virtual_beacon_searchable_encryption_example/__init__.py b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/virtual_beacon_searchable_encryption_example/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/virtual_beacon_searchable_encryption_example/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Stub to allow relative imports of examples from tests.""" diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/test_virtual_beacon_searchable_encryption_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/virtual_beacon_searchable_encryption_example/test_with_with_encrypted_client.py similarity index 72% rename from Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/test_virtual_beacon_searchable_encryption_example.py rename to Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/virtual_beacon_searchable_encryption_example/test_with_with_encrypted_client.py index 2ecda0a48..167bd4411 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/test_virtual_beacon_searchable_encryption_example.py +++ b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/virtual_beacon_searchable_encryption_example/test_with_with_encrypted_client.py @@ -5,13 +5,12 @@ import pytest -from DynamoDBEncryption.src.create_keystore_key_example import keystore_create_key -from DynamoDBEncryption.src.searchable_encryption.virtual_beacon_searchable_encryption_example import ( - put_item_query_item_with_virtual_beacon, +from ....src.create_keystore_key_example import keystore_create_key +from ....src.searchable_encryption.virtual_beacon_searchable_encryption_example.with_encrypted_client import ( + virtual_beacon_client_example ) - -from ..cleanup import delete_branch_key -from .searchable_encryption_test_utils import ( +from ...cleanup import delete_branch_key +from ..searchable_encryption_test_utils import ( SIMPLE_BEACON_TEST_DDB_TABLE_NAME, TEST_BRANCH_KEY_WRAPPING_KMS_KEY_ARN, TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME, @@ -21,7 +20,7 @@ pytestmark = [pytest.mark.examples] -def test_virtual_beacon_example(): +def test_virtual_beacon_client_example(): """Test virtual beacon searchable encryption example.""" # Create new branch key for test key_id = keystore_create_key( @@ -32,7 +31,7 @@ def test_virtual_beacon_example(): # our test fails due to eventual consistency issues. time.sleep(5) - put_item_query_item_with_virtual_beacon( + virtual_beacon_client_example( SIMPLE_BEACON_TEST_DDB_TABLE_NAME, key_id, TEST_BRANCH_KEY_WRAPPING_KMS_KEY_ARN, diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/virtual_beacon_searchable_encryption_example/test_with_with_encrypted_table.py b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/virtual_beacon_searchable_encryption_example/test_with_with_encrypted_table.py new file mode 100644 index 000000000..048e33627 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/virtual_beacon_searchable_encryption_example/test_with_with_encrypted_table.py @@ -0,0 +1,42 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test virtual beacon searchable encryption example.""" +import time + +import pytest + +from ....src.create_keystore_key_example import keystore_create_key +from ....src.searchable_encryption.virtual_beacon_searchable_encryption_example.with_encrypted_table import ( + virtual_beacon_table_example +) +from ...cleanup import delete_branch_key +from ..searchable_encryption_test_utils import ( + SIMPLE_BEACON_TEST_DDB_TABLE_NAME, + TEST_BRANCH_KEY_WRAPPING_KMS_KEY_ARN, + TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME, + TEST_LOGICAL_KEYSTORE_NAME, +) + +pytestmark = [pytest.mark.examples] + + +def test_virtual_beacon_table_example(): + """Test virtual beacon searchable encryption example.""" + # Create new branch key for test + key_id = keystore_create_key( + TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME, TEST_LOGICAL_KEYSTORE_NAME, TEST_BRANCH_KEY_WRAPPING_KMS_KEY_ARN + ) + + # Key creation is eventually consistent, so wait 5 seconds to decrease the likelihood + # our test fails due to eventual consistency issues. + time.sleep(5) + + virtual_beacon_table_example( + SIMPLE_BEACON_TEST_DDB_TABLE_NAME, + key_id, + TEST_BRANCH_KEY_WRAPPING_KMS_KEY_ARN, + TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME, + ) + + # Cleanup Branch Key + delete_branch_key(key_id, TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME, None) From 9a56148baee763a90604357224d6da32eb8bcdbc Mon Sep 17 00:00:00 2001 From: Darwin Chowdary <39110935+imabhichow@users.noreply.github.com> Date: Thu, 5 Jun 2025 10:42:27 -0700 Subject: [PATCH 3/9] linter --- .../src/scan_error_example/encryption_config.py | 3 ++- .../src/scan_error_example/with_encrypted_client.py | 2 ++ .../src/scan_error_example/with_encrypted_table.py | 2 ++ .../with_encrypted_table.py | 6 ------ .../with_encrypted_table.py | 6 ------ .../test/scan_error_example/test_with_encrypted_client.py | 1 + .../test/scan_error_example/test_with_encrypted_table.py | 1 + .../test_with_with_encrypted_client.py | 2 +- .../test_with_with_encrypted_table.py | 2 +- .../test_with_with_encrypted_client.py | 2 +- .../test_with_with_encrypted_table.py | 2 +- .../test_with_with_encrypted_client.py | 2 +- .../test_with_with_encrypted_table.py | 2 +- 13 files changed, 14 insertions(+), 19 deletions(-) diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/scan_error_example/encryption_config.py b/Examples/runtimes/python/DynamoDBEncryption/src/scan_error_example/encryption_config.py index 0fae116d1..60d936273 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/src/scan_error_example/encryption_config.py +++ b/Examples/runtimes/python/DynamoDBEncryption/src/scan_error_example/encryption_config.py @@ -13,7 +13,6 @@ from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig from aws_cryptographic_material_providers.mpl.errors import CollectionOfErrors from aws_cryptographic_material_providers.mpl.models import CreateAwsKmsMrkMultiKeyringInput, DBEAlgorithmSuiteId -from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient from aws_dbesdk_dynamodb.structures.dynamodb import ( DynamoDbTableEncryptionConfig, DynamoDbTablesEncryptionConfig, @@ -22,6 +21,7 @@ CryptoAction, ) + def print_exception(e: Exception, indent: str = ""): """ Print exception and any nested CollectionOfErrors. @@ -38,6 +38,7 @@ def print_exception(e: Exception, indent: str = ""): for err in e.list(): print_exception(err, indent + " ") + def create_encryption_config(kms_key_id: str, ddb_table_name: str) -> DynamoDbTablesEncryptionConfig: """ Create the encryption configuration for DynamoDB encryption using raw AES keyring. diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/scan_error_example/with_encrypted_client.py b/Examples/runtimes/python/DynamoDBEncryption/src/scan_error_example/with_encrypted_client.py index 7334ef76d..6702a2e13 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/src/scan_error_example/with_encrypted_client.py +++ b/Examples/runtimes/python/DynamoDBEncryption/src/scan_error_example/with_encrypted_client.py @@ -15,8 +15,10 @@ """ import boto3 from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient + from .encryption_config import create_encryption_config, print_exception + def scan_error_with_client(kms_key_id: str, ddb_table_name: str): """ Demonstrate handling scan errors with EncryptedClient. diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/scan_error_example/with_encrypted_table.py b/Examples/runtimes/python/DynamoDBEncryption/src/scan_error_example/with_encrypted_table.py index 816103a19..f1ebfb515 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/src/scan_error_example/with_encrypted_table.py +++ b/Examples/runtimes/python/DynamoDBEncryption/src/scan_error_example/with_encrypted_table.py @@ -15,8 +15,10 @@ """ import boto3 from aws_dbesdk_dynamodb.encrypted.table import EncryptedTable + from .encryption_config import create_encryption_config, print_exception + def scan_error_with_table(kms_key_id: str, ddb_table_name: str): """ Demonstrate handling scan errors with EncryptedClient. diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/compound_beacon_searchable_encryption_example/with_encrypted_table.py b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/compound_beacon_searchable_encryption_example/with_encrypted_table.py index 2f4797b63..9e132dd20 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/compound_beacon_searchable_encryption_example/with_encrypted_table.py +++ b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/compound_beacon_searchable_encryption_example/with_encrypted_table.py @@ -39,12 +39,6 @@ import boto3 from aws_dbesdk_dynamodb.encrypted.table import EncryptedTable -from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_transforms.client import ( - DynamoDbEncryptionTransforms, -) -from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_transforms.models import ( - ResolveAttributesInput, -) from DynamoDBEncryption.src.searchable_encryption.compound_beacon_searchable_encryption_example.beacon_config import ( GSI_NAME, diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/virtual_beacon_searchable_encryption_example/with_encrypted_table.py b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/virtual_beacon_searchable_encryption_example/with_encrypted_table.py index fa74cd773..6514316aa 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/virtual_beacon_searchable_encryption_example/with_encrypted_table.py +++ b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/virtual_beacon_searchable_encryption_example/with_encrypted_table.py @@ -98,12 +98,6 @@ import boto3 from aws_dbesdk_dynamodb.encrypted.table import EncryptedTable -from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_transforms.client import ( - DynamoDbEncryptionTransforms, -) -from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_transforms.models import ( - ResolveAttributesInput, -) from DynamoDBEncryption.src.searchable_encryption.virtual_beacon_searchable_encryption_example.beacon_config import ( setup_beacon_config, diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/scan_error_example/test_with_encrypted_client.py b/Examples/runtimes/python/DynamoDBEncryption/test/scan_error_example/test_with_encrypted_client.py index 2791cfc36..e0caf9709 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/test/scan_error_example/test_with_encrypted_client.py +++ b/Examples/runtimes/python/DynamoDBEncryption/test/scan_error_example/test_with_encrypted_client.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 """Test scan error example.""" import pytest + from ...src.scan_error_example.with_encrypted_client import scan_error_with_client from ..test_utils import TEST_DDB_TABLE_NAME, TEST_KMS_KEY_ID diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/scan_error_example/test_with_encrypted_table.py b/Examples/runtimes/python/DynamoDBEncryption/test/scan_error_example/test_with_encrypted_table.py index a448bb02c..ff11c20a1 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/test/scan_error_example/test_with_encrypted_table.py +++ b/Examples/runtimes/python/DynamoDBEncryption/test/scan_error_example/test_with_encrypted_table.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 """Test scan error example.""" import pytest + from ...src.scan_error_example.with_encrypted_table import scan_error_with_table from ..test_utils import TEST_DDB_TABLE_NAME, TEST_KMS_KEY_ID diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/basic_searchable_encryption_example/test_with_with_encrypted_client.py b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/basic_searchable_encryption_example/test_with_with_encrypted_client.py index c8ba23769..75dcc56a5 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/basic_searchable_encryption_example/test_with_with_encrypted_client.py +++ b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/basic_searchable_encryption_example/test_with_with_encrypted_client.py @@ -7,7 +7,7 @@ from ....src.create_keystore_key_example import keystore_create_key from ....src.searchable_encryption.basic_searchable_encryption_example.with_encrypted_client import ( - basic_searchable_encryption_client_example + basic_searchable_encryption_client_example, ) from ...cleanup import delete_branch_key from ..searchable_encryption_test_utils import ( diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/basic_searchable_encryption_example/test_with_with_encrypted_table.py b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/basic_searchable_encryption_example/test_with_with_encrypted_table.py index b811d67c6..d3a97485a 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/basic_searchable_encryption_example/test_with_with_encrypted_table.py +++ b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/basic_searchable_encryption_example/test_with_with_encrypted_table.py @@ -7,7 +7,7 @@ from ....src.create_keystore_key_example import keystore_create_key from ....src.searchable_encryption.basic_searchable_encryption_example.with_encrypted_table import ( - basic_searchable_encryption_table_example + basic_searchable_encryption_table_example, ) from ...cleanup import delete_branch_key from ..searchable_encryption_test_utils import ( diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/compound_beacon_searchable_encryption_example/test_with_with_encrypted_client.py b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/compound_beacon_searchable_encryption_example/test_with_with_encrypted_client.py index 8453b630e..351e9f417 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/compound_beacon_searchable_encryption_example/test_with_with_encrypted_client.py +++ b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/compound_beacon_searchable_encryption_example/test_with_with_encrypted_client.py @@ -7,7 +7,7 @@ from ....src.create_keystore_key_example import keystore_create_key from ....src.searchable_encryption.compound_beacon_searchable_encryption_example.with_encrypted_client import ( - compound_beacon_client_example + compound_beacon_client_example, ) from ...cleanup import delete_branch_key from ..searchable_encryption_test_utils import ( diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/compound_beacon_searchable_encryption_example/test_with_with_encrypted_table.py b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/compound_beacon_searchable_encryption_example/test_with_with_encrypted_table.py index ffbfa4b41..b7ea7b1ae 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/compound_beacon_searchable_encryption_example/test_with_with_encrypted_table.py +++ b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/compound_beacon_searchable_encryption_example/test_with_with_encrypted_table.py @@ -7,7 +7,7 @@ from ....src.create_keystore_key_example import keystore_create_key from ....src.searchable_encryption.compound_beacon_searchable_encryption_example.with_encrypted_table import ( - compound_beacon_table_example + compound_beacon_table_example, ) from ...cleanup import delete_branch_key from ..searchable_encryption_test_utils import ( diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/virtual_beacon_searchable_encryption_example/test_with_with_encrypted_client.py b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/virtual_beacon_searchable_encryption_example/test_with_with_encrypted_client.py index 167bd4411..254792c51 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/virtual_beacon_searchable_encryption_example/test_with_with_encrypted_client.py +++ b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/virtual_beacon_searchable_encryption_example/test_with_with_encrypted_client.py @@ -7,7 +7,7 @@ from ....src.create_keystore_key_example import keystore_create_key from ....src.searchable_encryption.virtual_beacon_searchable_encryption_example.with_encrypted_client import ( - virtual_beacon_client_example + virtual_beacon_client_example, ) from ...cleanup import delete_branch_key from ..searchable_encryption_test_utils import ( diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/virtual_beacon_searchable_encryption_example/test_with_with_encrypted_table.py b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/virtual_beacon_searchable_encryption_example/test_with_with_encrypted_table.py index 048e33627..9b1828b71 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/virtual_beacon_searchable_encryption_example/test_with_with_encrypted_table.py +++ b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/virtual_beacon_searchable_encryption_example/test_with_with_encrypted_table.py @@ -7,7 +7,7 @@ from ....src.create_keystore_key_example import keystore_create_key from ....src.searchable_encryption.virtual_beacon_searchable_encryption_example.with_encrypted_table import ( - virtual_beacon_table_example + virtual_beacon_table_example, ) from ...cleanup import delete_branch_key from ..searchable_encryption_test_utils import ( From 92655299c7c1114007434aa800eb056b3415383e Mon Sep 17 00:00:00 2001 From: Darwin Chowdary <39110935+imabhichow@users.noreply.github.com> Date: Fri, 6 Jun 2025 13:36:46 -0700 Subject: [PATCH 4/9] feedback --- .../keyring/raw_ecdh_keyring_example/with_encrypted_client.py | 2 +- .../raw_ecdh_keyring_example/test_with_encrypted_client.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_ecdh_keyring_example/with_encrypted_client.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_ecdh_keyring_example/with_encrypted_client.py index 10694b386..ce25df0bc 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_ecdh_keyring_example/with_encrypted_client.py +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_ecdh_keyring_example/with_encrypted_client.py @@ -46,7 +46,7 @@ ) -def raw_ecdh_keyring_get_item_put_item_with_ecnrypted_client(ddb_table_name: str, curve_spec: str): +def raw_ecdh_keyring_get_item_put_item_with_encrypted_client(ddb_table_name: str, curve_spec: str): """ Demonstrate using a raw ECDH keyring with static keys. diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/raw_ecdh_keyring_example/test_with_encrypted_client.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/raw_ecdh_keyring_example/test_with_encrypted_client.py index 1b79e4fd6..8d8b46068 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/raw_ecdh_keyring_example/test_with_encrypted_client.py +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/raw_ecdh_keyring_example/test_with_encrypted_client.py @@ -11,7 +11,7 @@ from ....src.keyring.raw_ecdh_keyring_example.with_encrypted_client import ( discovery_raw_ecdh_keyring_get_item_with_ecnrypted_client, ephemeral_raw_ecdh_keyring_put_item_with_ecnrypted_client, - raw_ecdh_keyring_get_item_put_item_with_ecnrypted_client, + raw_ecdh_keyring_get_item_put_item_with_encrypted_client, ) from ...test_utils import TEST_DDB_TABLE_NAME @@ -31,7 +31,7 @@ def test_raw_ecdh_keyring_get_item_put_item(): # Part of using these keyrings is knowing which curve the keys used in the key agreement # lie on. The keyring will fail if the keys do not lie on the configured curve. - raw_ecdh_keyring_get_item_put_item_with_ecnrypted_client(TEST_DDB_TABLE_NAME, ECDHCurveSpec.ECC_NIST_P256) + raw_ecdh_keyring_get_item_put_item_with_encrypted_client(TEST_DDB_TABLE_NAME, ECDHCurveSpec.ECC_NIST_P256) def test_ephemeral_raw_ecdh_keyring_put_item(): From 86192e23d7e64607b8a27f5c5cd282d1ed717d32 Mon Sep 17 00:00:00 2001 From: Darwin Chowdary <39110935+imabhichow@users.noreply.github.com> Date: Fri, 6 Jun 2025 13:44:07 -0700 Subject: [PATCH 5/9] feedback --- .../with_encrypted_client.py | 4 ++-- .../with_encrypted_table.py | 6 +++--- .../test_with_encrypted_client.py | 10 +++++----- .../test_with_encrypted_table.py | 14 +++++++------- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_ecdh_keyring_example/with_encrypted_client.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_ecdh_keyring_example/with_encrypted_client.py index ce25df0bc..3752fe4ba 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_ecdh_keyring_example/with_encrypted_client.py +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_ecdh_keyring_example/with_encrypted_client.py @@ -122,7 +122,7 @@ def raw_ecdh_keyring_get_item_put_item_with_encrypted_client(ddb_table_name: str put_get_example_with_keyring_with_encrypted_client(raw_ecdh_keyring, ddb_table_name) -def ephemeral_raw_ecdh_keyring_put_item_with_ecnrypted_client(ddb_table_name: str, curve_spec: str): +def ephemeral_raw_ecdh_keyring_put_item_with_encrypted_client(ddb_table_name: str, curve_spec: str): """ Demonstrate using a raw ECDH keyring with ephemeral keys. @@ -175,7 +175,7 @@ def ephemeral_raw_ecdh_keyring_put_item_with_ecnrypted_client(ddb_table_name: st put_example_with_keyring_with_encrypted_client(raw_ecdh_keyring, ddb_table_name) -def discovery_raw_ecdh_keyring_get_item_with_ecnrypted_client(ddb_table_name: str, curve_spec: str): +def discovery_raw_ecdh_keyring_get_item_with_encrypted_client(ddb_table_name: str, curve_spec: str): """ Demonstrate using a raw ECDH keyring with discovery. diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_ecdh_keyring_example/with_encrypted_table.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_ecdh_keyring_example/with_encrypted_table.py index 5b13e0fb9..9943bc45d 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_ecdh_keyring_example/with_encrypted_table.py +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_ecdh_keyring_example/with_encrypted_table.py @@ -46,7 +46,7 @@ ) -def raw_ecdh_keyring_get_item_put_item_with_ecnrypted_table(ddb_table_name: str, curve_spec: str): +def raw_ecdh_keyring_get_item_put_item_with_encrypted_table(ddb_table_name: str, curve_spec: str): """ Demonstrate using a raw ECDH keyring with static keys. @@ -122,7 +122,7 @@ def raw_ecdh_keyring_get_item_put_item_with_ecnrypted_table(ddb_table_name: str, put_get_example_with_keyring_with_encrypted_table(raw_ecdh_keyring, ddb_table_name) -def ephemeral_raw_ecdh_keyring_put_item_with_ecnrypted_table(ddb_table_name: str, curve_spec: str): +def ephemeral_raw_ecdh_keyring_put_item_with_encrypted_table(ddb_table_name: str, curve_spec: str): """ Demonstrate using a raw ECDH keyring with ephemeral keys. @@ -175,7 +175,7 @@ def ephemeral_raw_ecdh_keyring_put_item_with_ecnrypted_table(ddb_table_name: str put_example_with_keyring_with_encrypted_table(raw_ecdh_keyring, ddb_table_name) -def discovery_raw_ecdh_keyring_get_item_with_ecnrypted_table(ddb_table_name: str, curve_spec: str): +def discovery_raw_ecdh_keyring_get_item_with_encrypted_table(ddb_table_name: str, curve_spec: str): """ Demonstrate using a raw ECDH keyring with discovery. diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/raw_ecdh_keyring_example/test_with_encrypted_client.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/raw_ecdh_keyring_example/test_with_encrypted_client.py index 8d8b46068..cc55d9ddb 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/raw_ecdh_keyring_example/test_with_encrypted_client.py +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/raw_ecdh_keyring_example/test_with_encrypted_client.py @@ -9,8 +9,8 @@ should_generate_new_ecc_key_pairs, ) from ....src.keyring.raw_ecdh_keyring_example.with_encrypted_client import ( - discovery_raw_ecdh_keyring_get_item_with_ecnrypted_client, - ephemeral_raw_ecdh_keyring_put_item_with_ecnrypted_client, + discovery_raw_ecdh_keyring_get_item_with_encrypted_client, + ephemeral_raw_ecdh_keyring_put_item_with_encrypted_client, raw_ecdh_keyring_get_item_put_item_with_encrypted_client, ) from ...test_utils import TEST_DDB_TABLE_NAME @@ -46,7 +46,7 @@ def test_ephemeral_raw_ecdh_keyring_put_item(): # Part of using these keyrings is knowing which curve the keys used in the key agreement # lie on. The keyring will fail if the keys do not lie on the configured curve. - ephemeral_raw_ecdh_keyring_put_item_with_ecnrypted_client(TEST_DDB_TABLE_NAME, ECDHCurveSpec.ECC_NIST_P256) + ephemeral_raw_ecdh_keyring_put_item_with_encrypted_client(TEST_DDB_TABLE_NAME, ECDHCurveSpec.ECC_NIST_P256) def test_discovery_raw_ecdh_keyring_get_item(): @@ -68,9 +68,9 @@ def test_discovery_raw_ecdh_keyring_get_item(): # In this call we are writing a record that is written with an ephemeral sender key pair. # The recipient will be able to decrypt the message - ephemeral_raw_ecdh_keyring_put_item_with_ecnrypted_client(TEST_DDB_TABLE_NAME, ECDHCurveSpec.ECC_NIST_P256) + ephemeral_raw_ecdh_keyring_put_item_with_encrypted_client(TEST_DDB_TABLE_NAME, ECDHCurveSpec.ECC_NIST_P256) # In this call we are reading a record that was written with the recipient's public key. # It will use the recipient's private key and the sender's public key stored in the message to # calculate the appropriate shared secret to successfully decrypt the message. - discovery_raw_ecdh_keyring_get_item_with_ecnrypted_client(TEST_DDB_TABLE_NAME, ECDHCurveSpec.ECC_NIST_P256) + discovery_raw_ecdh_keyring_get_item_with_encrypted_client(TEST_DDB_TABLE_NAME, ECDHCurveSpec.ECC_NIST_P256) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/raw_ecdh_keyring_example/test_with_encrypted_table.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/raw_ecdh_keyring_example/test_with_encrypted_table.py index 2695931f5..5a9be8623 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/raw_ecdh_keyring_example/test_with_encrypted_table.py +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/raw_ecdh_keyring_example/test_with_encrypted_table.py @@ -9,9 +9,9 @@ should_generate_new_ecc_key_pairs, ) from ....src.keyring.raw_ecdh_keyring_example.with_encrypted_table import ( - discovery_raw_ecdh_keyring_get_item_with_ecnrypted_table, - ephemeral_raw_ecdh_keyring_put_item_with_ecnrypted_table, - raw_ecdh_keyring_get_item_put_item_with_ecnrypted_table, + discovery_raw_ecdh_keyring_get_item_with_encrypted_table, + ephemeral_raw_ecdh_keyring_put_item_with_encrypted_table, + raw_ecdh_keyring_get_item_put_item_with_encrypted_table, ) from ...test_utils import TEST_DDB_TABLE_NAME @@ -31,7 +31,7 @@ def test_raw_ecdh_keyring_get_item_put_item_with_table(): # Part of using these keyrings is knowing which curve the keys used in the key agreement # lie on. The keyring will fail if the keys do not lie on the configured curve. - raw_ecdh_keyring_get_item_put_item_with_ecnrypted_table(TEST_DDB_TABLE_NAME, ECDHCurveSpec.ECC_NIST_P256) + raw_ecdh_keyring_get_item_put_item_with_encrypted_table(TEST_DDB_TABLE_NAME, ECDHCurveSpec.ECC_NIST_P256) def test_ephemeral_raw_ecdh_keyring_put_item_with_table(): @@ -46,7 +46,7 @@ def test_ephemeral_raw_ecdh_keyring_put_item_with_table(): # Part of using these keyrings is knowing which curve the keys used in the key agreement # lie on. The keyring will fail if the keys do not lie on the configured curve. - ephemeral_raw_ecdh_keyring_put_item_with_ecnrypted_table(TEST_DDB_TABLE_NAME, ECDHCurveSpec.ECC_NIST_P256) + ephemeral_raw_ecdh_keyring_put_item_with_encrypted_table(TEST_DDB_TABLE_NAME, ECDHCurveSpec.ECC_NIST_P256) def test_discovery_raw_ecdh_keyring_get_item_with_table(): @@ -68,9 +68,9 @@ def test_discovery_raw_ecdh_keyring_get_item_with_table(): # In this call we are writing a record that is written with an ephemeral sender key pair. # The recipient will be able to decrypt the message - ephemeral_raw_ecdh_keyring_put_item_with_ecnrypted_table(TEST_DDB_TABLE_NAME, ECDHCurveSpec.ECC_NIST_P256) + ephemeral_raw_ecdh_keyring_put_item_with_encrypted_table(TEST_DDB_TABLE_NAME, ECDHCurveSpec.ECC_NIST_P256) # In this call we are reading a record that was written with the recipient's public key. # It will use the recipient's private key and the sender's public key stored in the message to # calculate the appropriate shared secret to successfully decrypt the message. - discovery_raw_ecdh_keyring_get_item_with_ecnrypted_table(TEST_DDB_TABLE_NAME, ECDHCurveSpec.ECC_NIST_P256) + discovery_raw_ecdh_keyring_get_item_with_encrypted_table(TEST_DDB_TABLE_NAME, ECDHCurveSpec.ECC_NIST_P256) From b4da3a078e1d4fe79d49b9f801f799755efbb2bd Mon Sep 17 00:00:00 2001 From: Darwin Chowdary <39110935+imabhichow@users.noreply.github.com> Date: Fri, 6 Jun 2025 15:02:09 -0700 Subject: [PATCH 6/9] fix comments/docstring --- .../with_encrypted_client.py | 23 +++++++++++++++++ .../with_encrypted_table.py | 25 ++++++++++++++++++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/hierarchical_keyring_example/with_encrypted_client.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/hierarchical_keyring_example/with_encrypted_client.py index 76aa4d92d..6132c93d2 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/hierarchical_keyring_example/with_encrypted_client.py +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/hierarchical_keyring_example/with_encrypted_client.py @@ -9,6 +9,29 @@ These branch keys are used to protect your data keys, and these branch keys are themselves protected by a root KMS Key. +Establishing a key hierarchy like this has two benefits: + +First, by caching the branch key material, and only calling back +to KMS to re-establish authentication regularly according to your configured TTL, +you limit how often you need to call back to KMS to protect your data. +This is a performance/security tradeoff, where your authentication, audit, and +logging from KMS is no longer one-to-one with every encrypt or decrypt call. +However, the benefit is that you no longer have to make a +network call to KMS for every encrypt or decrypt. + +Second, this key hierarchy makes it easy to hold multi-tenant data +that is isolated per branch key in a single DynamoDb table. +You can create a branch key for each tenant in your table, +and encrypt all that tenant's data under that distinct branch key. +On decrypt, you can either statically configure a single branch key +to ensure you are restricting decryption to a single tenant, +or you can implement an interface that lets you map the primary key on your items +to the branch key that should be responsible for decrypting that data. + +This example then demonstrates configuring a Hierarchical Keyring +with a Branch Key ID Supplier to encrypt and decrypt data for +two separate tenants. + Running this example requires access to the DDB Table whose name is provided in CLI arguments. This table must be configured with the following diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/hierarchical_keyring_example/with_encrypted_table.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/hierarchical_keyring_example/with_encrypted_table.py index b42787a85..0f6ea7f57 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/hierarchical_keyring_example/with_encrypted_table.py +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/hierarchical_keyring_example/with_encrypted_table.py @@ -3,12 +3,35 @@ """ Example demonstrating DynamoDb Encryption using a Hierarchical Keyring with EncryptedTable. -This example sets up DynamoDb Encryption for the AWS SDK table +This example sets up DynamoDb Encryption for the AWS SDK client using the Hierarchical Keyring, which establishes a key hierarchy where "branch" keys are persisted in DynamoDb. These branch keys are used to protect your data keys, and these branch keys are themselves protected by a root KMS Key. +Establishing a key hierarchy like this has two benefits: + +First, by caching the branch key material, and only calling back +to KMS to re-establish authentication regularly according to your configured TTL, +you limit how often you need to call back to KMS to protect your data. +This is a performance/security tradeoff, where your authentication, audit, and +logging from KMS is no longer one-to-one with every encrypt or decrypt call. +However, the benefit is that you no longer have to make a +network call to KMS for every encrypt or decrypt. + +Second, this key hierarchy makes it easy to hold multi-tenant data +that is isolated per branch key in a single DynamoDb table. +You can create a branch key for each tenant in your table, +and encrypt all that tenant's data under that distinct branch key. +On decrypt, you can either statically configure a single branch key +to ensure you are restricting decryption to a single tenant, +or you can implement an interface that lets you map the primary key on your items +to the branch key that should be responsible for decrypting that data. + +This example then demonstrates configuring a Hierarchical Keyring +with a Branch Key ID Supplier to encrypt and decrypt data for +two separate tenants. + Running this example requires access to the DDB Table whose name is provided in CLI arguments. This table must be configured with the following From 4b52383a0e2cb95307b36afda52e47705e339fb2 Mon Sep 17 00:00:00 2001 From: Darwin Chowdary <39110935+imabhichow@users.noreply.github.com> Date: Fri, 6 Jun 2025 15:30:09 -0700 Subject: [PATCH 7/9] client -> table resource --- .../hierarchical_keyring_example/with_encrypted_table.py | 2 +- .../keyring/kms_ecdh_keyring_example/with_encrypted_table.py | 2 +- .../keyring/raw_ecdh_keyring_example/with_encrypted_table.py | 2 +- .../with_encrypted_table.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/hierarchical_keyring_example/with_encrypted_table.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/hierarchical_keyring_example/with_encrypted_table.py index 0f6ea7f57..11f370ff7 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/hierarchical_keyring_example/with_encrypted_table.py +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/hierarchical_keyring_example/with_encrypted_table.py @@ -3,7 +3,7 @@ """ Example demonstrating DynamoDb Encryption using a Hierarchical Keyring with EncryptedTable. -This example sets up DynamoDb Encryption for the AWS SDK client +This example sets up DynamoDb Encryption for the AWS SDK table resource using the Hierarchical Keyring, which establishes a key hierarchy where "branch" keys are persisted in DynamoDb. These branch keys are used to protect your data keys, diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_ecdh_keyring_example/with_encrypted_table.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_ecdh_keyring_example/with_encrypted_table.py index 266776fa6..03674907f 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_ecdh_keyring_example/with_encrypted_table.py +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_ecdh_keyring_example/with_encrypted_table.py @@ -3,7 +3,7 @@ """ Examples demonstrating DynamoDb Encryption using an AWS KMS ECDH Keyring with EncryptedTable. -These examples set up DynamoDb Encryption for the AWS SDK client +These examples set up DynamoDb Encryption for the AWS SDK table resource using the AWS KMS ECDH Keyring. This keyring, depending on its KeyAgreement scheme, takes in the sender's KMS ECC Key ARN, and the recipient's ECC Public Key to derive a shared secret. The keyring uses the shared secret to derive a data key to protect the diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_ecdh_keyring_example/with_encrypted_table.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_ecdh_keyring_example/with_encrypted_table.py index 9943bc45d..a6d5b13f2 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_ecdh_keyring_example/with_encrypted_table.py +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_ecdh_keyring_example/with_encrypted_table.py @@ -1,7 +1,7 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 """ -These examples set up DynamoDb Encryption for the AWS SDK client using the raw ECDH Keyring with EncrypedTable. +These examples set up DynamoDb Encryption for the AWS SDK table resource using the raw ECDH Keyring with EncrypedTable. This keyring, depending on its KeyAgreement scheme, takes in the sender's ECC private key, and the recipient's ECC Public Key to derive a shared secret. diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/shared_cache_across_hierarchical_keyrings_example/with_encrypted_table.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/shared_cache_across_hierarchical_keyrings_example/with_encrypted_table.py index b253e8eb6..dde5f0873 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/shared_cache_across_hierarchical_keyrings_example/with_encrypted_table.py +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/shared_cache_across_hierarchical_keyrings_example/with_encrypted_table.py @@ -46,7 +46,7 @@ Please make sure that you set all of Partition ID, Logical Key Store Name and Branch Key ID to be the same for two Hierarchical Keyrings if and only if you want them to share cache entries. -This example sets up DynamoDb Encryption for the AWS SDK client using the Hierarchical +This example sets up DynamoDb Encryption for the AWS SDK table resource using the Hierarchical Keyring, which establishes a key hierarchy where "branch" keys are persisted in DynamoDb. These branch keys are used to protect your data keys, and these branch keys are themselves protected by a root KMS Key. From 063aecba17c76e0d021fbab107a762d83b583958 Mon Sep 17 00:00:00 2001 From: Darwin Chowdary <39110935+imabhichow@users.noreply.github.com> Date: Fri, 6 Jun 2025 15:35:34 -0700 Subject: [PATCH 8/9] capitalize T --- .../hierarchical_keyring_example/with_encrypted_table.py | 2 +- .../keyring/kms_ecdh_keyring_example/with_encrypted_table.py | 2 +- .../keyring/raw_ecdh_keyring_example/with_encrypted_table.py | 2 +- .../with_encrypted_table.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/hierarchical_keyring_example/with_encrypted_table.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/hierarchical_keyring_example/with_encrypted_table.py index 11f370ff7..3765c4994 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/hierarchical_keyring_example/with_encrypted_table.py +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/hierarchical_keyring_example/with_encrypted_table.py @@ -3,7 +3,7 @@ """ Example demonstrating DynamoDb Encryption using a Hierarchical Keyring with EncryptedTable. -This example sets up DynamoDb Encryption for the AWS SDK table resource +This example sets up DynamoDb Encryption for the AWS SDK Table resource using the Hierarchical Keyring, which establishes a key hierarchy where "branch" keys are persisted in DynamoDb. These branch keys are used to protect your data keys, diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_ecdh_keyring_example/with_encrypted_table.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_ecdh_keyring_example/with_encrypted_table.py index 03674907f..3749a2e87 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_ecdh_keyring_example/with_encrypted_table.py +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_ecdh_keyring_example/with_encrypted_table.py @@ -3,7 +3,7 @@ """ Examples demonstrating DynamoDb Encryption using an AWS KMS ECDH Keyring with EncryptedTable. -These examples set up DynamoDb Encryption for the AWS SDK table resource +These examples set up DynamoDb Encryption for the AWS SDK Table resource using the AWS KMS ECDH Keyring. This keyring, depending on its KeyAgreement scheme, takes in the sender's KMS ECC Key ARN, and the recipient's ECC Public Key to derive a shared secret. The keyring uses the shared secret to derive a data key to protect the diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_ecdh_keyring_example/with_encrypted_table.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_ecdh_keyring_example/with_encrypted_table.py index a6d5b13f2..758458bcf 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_ecdh_keyring_example/with_encrypted_table.py +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_ecdh_keyring_example/with_encrypted_table.py @@ -1,7 +1,7 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 """ -These examples set up DynamoDb Encryption for the AWS SDK table resource using the raw ECDH Keyring with EncrypedTable. +These examples set up DynamoDb Encryption for the AWS SDK Table resource using the raw ECDH Keyring with EncrypedTable. This keyring, depending on its KeyAgreement scheme, takes in the sender's ECC private key, and the recipient's ECC Public Key to derive a shared secret. diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/shared_cache_across_hierarchical_keyrings_example/with_encrypted_table.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/shared_cache_across_hierarchical_keyrings_example/with_encrypted_table.py index dde5f0873..89173035a 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/shared_cache_across_hierarchical_keyrings_example/with_encrypted_table.py +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/shared_cache_across_hierarchical_keyrings_example/with_encrypted_table.py @@ -46,7 +46,7 @@ Please make sure that you set all of Partition ID, Logical Key Store Name and Branch Key ID to be the same for two Hierarchical Keyrings if and only if you want them to share cache entries. -This example sets up DynamoDb Encryption for the AWS SDK table resource using the Hierarchical +This example sets up DynamoDb Encryption for the AWS SDK Table resource using the Hierarchical Keyring, which establishes a key hierarchy where "branch" keys are persisted in DynamoDb. These branch keys are used to protect your data keys, and these branch keys are themselves protected by a root KMS Key. From 70fa6b0b3de2194a0b0e4a1a621e074b5b9eb4b0 Mon Sep 17 00:00:00 2001 From: Darwin Chowdary <39110935+imabhichow@users.noreply.github.com> Date: Fri, 6 Jun 2025 15:39:14 -0700 Subject: [PATCH 9/9] Apply suggestions from code review Co-authored-by: Lucas McDonald --- .../with_encrypted_client.py | 2 -- .../with_encrypted_table.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/virtual_beacon_searchable_encryption_example/with_encrypted_client.py b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/virtual_beacon_searchable_encryption_example/with_encrypted_client.py index bae7a1a39..43ecd59ce 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/virtual_beacon_searchable_encryption_example/with_encrypted_client.py +++ b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/virtual_beacon_searchable_encryption_example/with_encrypted_client.py @@ -1,7 +1,5 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: Apache-2.0 """ Example demonstrating DynamoDB encryption using virtual beacons with EncryptedClient. diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/virtual_beacon_searchable_encryption_example/with_encrypted_table.py b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/virtual_beacon_searchable_encryption_example/with_encrypted_table.py index 6514316aa..68cee7d96 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/virtual_beacon_searchable_encryption_example/with_encrypted_table.py +++ b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/virtual_beacon_searchable_encryption_example/with_encrypted_table.py @@ -1,7 +1,5 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: Apache-2.0 """ Example demonstrating DynamoDB encryption using virtual beacons with EncryptedTable.