diff --git a/Examples/runtimes/java/DynamoDbEncryption/src/main/java/software/amazon/cryptography/examples/keyring/SharedCacheAcrossHierarchicalKeyringsExample.java b/Examples/runtimes/java/DynamoDbEncryption/src/main/java/software/amazon/cryptography/examples/keyring/SharedCacheAcrossHierarchicalKeyringsExample.java index dfed4b84d..62fb6d711 100644 --- a/Examples/runtimes/java/DynamoDbEncryption/src/main/java/software/amazon/cryptography/examples/keyring/SharedCacheAcrossHierarchicalKeyringsExample.java +++ b/Examples/runtimes/java/DynamoDbEncryption/src/main/java/software/amazon/cryptography/examples/keyring/SharedCacheAcrossHierarchicalKeyringsExample.java @@ -180,7 +180,7 @@ public static void SharedCacheAcrossHierarchicalKeyringsGetItemPutItem( final IKeyring hierarchicalKeyring1 = matProv.CreateAwsKmsHierarchicalKeyring(keyringInput1); - // 4. Configure which attributes are encrypted and/or signed when writing new items. + // 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 @@ -194,14 +194,14 @@ public static void SharedCacheAcrossHierarchicalKeyringsGetItemPutItem( CryptoAction.ENCRYPT_AND_SIGN ); - // 5. Get the DDB Client for Hierarchical Keyring 1. + // 6. Get the DDB Client for Hierarchical Keyring 1. final DynamoDbClient ddbClient1 = GetDdbClient( ddbTableName, hierarchicalKeyring1, attributeActionsOnEncrypt ); - // 6. Encrypt Decrypt roundtrip with ddbClient1 + // 7. Encrypt Decrypt roundtrip with ddbClient1 PutGetItems(ddbTableName, ddbClient1); // Through the above encrypt and decrypt roundtrip, the cache will be populated and @@ -210,7 +210,7 @@ public static void SharedCacheAcrossHierarchicalKeyringsGetItemPutItem( // - Same Logical Key Store Name of the Key Store for the Hierarchical Keyring // - Same Branch Key ID - // 7. Configure your KeyStore resource keystore2. + // 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 keyStoreTableName is the physical Key Store, @@ -243,7 +243,7 @@ public static void SharedCacheAcrossHierarchicalKeyringsGetItemPutItem( ) .build(); - // 8. Create the Hierarchical Keyring HK2 with Key Store instance K2, the shared Cache + // 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). @@ -262,14 +262,14 @@ public static void SharedCacheAcrossHierarchicalKeyringsGetItemPutItem( final IKeyring hierarchicalKeyring2 = matProv.CreateAwsKmsHierarchicalKeyring(keyringInput2); - // 9. Get the DDB Client for Hierarchical Keyring 2. + // 10. Get the DDB Client for Hierarchical Keyring 2. final DynamoDbClient ddbClient2 = GetDdbClient( ddbTableName, hierarchicalKeyring2, attributeActionsOnEncrypt ); - // 10. Encrypt Decrypt roundtrip with ddbClient2 + // 11. Encrypt Decrypt roundtrip with ddbClient2 PutGetItems(ddbTableName, ddbClient2); } diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/create_keystore_key_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/create_keystore_key_example.py index cf4c6d16e..6957290f8 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/src/create_keystore_key_example.py +++ b/Examples/runtimes/python/DynamoDBEncryption/src/create_keystore_key_example.py @@ -3,20 +3,18 @@ """ Example for creating a new key in a KeyStore. -The Hierarchical Keyring Example and Searchable Encryption Examples -rely on the existence of a DDB-backed key store with pre-existing -branch key material or beacon key material. +The Hierarchical Keyring Example and Searchable Encryption Examples rely on the +existence of a DDB-backed key store with pre-existing branch key material or +beacon key material. -See the "Create KeyStore Table Example" for how to first set up -the DDB Table that will back this KeyStore. +See the "Create KeyStore Table Example" for how to first set up the DDB Table +that will back this KeyStore. -This example demonstrates configuring a KeyStore and then -using a helper method to create a branch key and beacon key -that share the same Id, then return that Id. -We will always create a new beacon key alongside a new branch key, -even if you are not using searchable encryption. +Demonstrates configuring a KeyStore and using a helper method to create a branch +key and beacon key that share the same Id. A new beacon key is always created +alongside a new branch key, even if searchable encryption is not being used. -This key creation should occur within your control plane. +Note: This key creation should occur within your control plane. """ import boto3 diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/create_keystore_table_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/create_keystore_table_example.py new file mode 100644 index 000000000..d154e33c2 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/create_keystore_table_example.py @@ -0,0 +1,59 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Example for creating a DynamoDB table for use as a KeyStore. + +The Hierarchical Keyring Example and Searchable Encryption Examples rely on the +existence of a DDB-backed key store with pre-existing branch key material or +beacon key material. + +Shows how to configure a KeyStore and use a helper method to create the DDB table +that will be used to persist branch keys and beacons keys for this KeyStore. + +This table creation should occur within your control plane and only needs to occur +once. While not demonstrated in this example, you should additionally use the +`VersionKey` API on the KeyStore to periodically rotate your branch key material. +""" + +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 ( + CreateKeyStoreInput, + KMSConfigurationKmsKeyArn, +) + + +def keystore_create_table(keystore_table_name: str, logical_keystore_name: str, kms_key_arn: str): + """ + Create KeyStore Table Example. + + :param keystore_table_name: The name of the DynamoDB table to create + :param logical_keystore_name: The logical name for this keystore + :param kms_key_arn: The ARN of the KMS key to use for protecting branch keys + """ + # 1. Configure your KeyStore resource. + # `ddb_table_name` is the name you want for the DDB table that + # will back your keystore. + # `kms_key_arn` is the KMS Key that will protect your branch keys and beacon keys + # when they are stored in your DDB table. + keystore = KeyStore( + config=KeyStoreConfig( + ddb_client=boto3.client("dynamodb"), + ddb_table_name=keystore_table_name, + logical_key_store_name=logical_keystore_name, + kms_client=boto3.client("kms"), + kms_configuration=KMSConfigurationKmsKeyArn(kms_key_arn), + ) + ) + + # 2. Create the DynamoDb table that will store the branch keys and beacon keys. + # This checks if the correct table already exists at `ddb_table_name` + # by using the DescribeTable API. If no table exists, + # it will create one. If a table exists, it will verify + # the table's configuration and will error if the configuration is incorrect. + keystore.create_key_store(input=CreateKeyStoreInput()) + # It may take a couple of minutes for the table to become ACTIVE, + # at which point it is ready to store branch and beacon keys. + # See the Create KeyStore Key Example for how to populate + # this table. 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/item_encryptor/encrypt_decrypt_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/item_encryptor/encrypt_decrypt_example.py index 40e5e15b0..daf8082f0 100644 --- a/Examples/runtimes/python/DynamoDBEncryption/src/item_encryptor/encrypt_decrypt_example.py +++ b/Examples/runtimes/python/DynamoDBEncryption/src/item_encryptor/encrypt_decrypt_example.py @@ -45,7 +45,7 @@ ) -def encrypt_decrypt_example(kms_key_id: str, ddb_table_name: str) -> None: +def encrypt_decrypt_example(kms_key_id: str, ddb_table_name: str): """Encrypt and decrypt an item with an ItemEncryptor.""" # 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. diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/__init__.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/__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/example_branch_key_id_supplier.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/example_branch_key_id_supplier.py new file mode 100644 index 000000000..f16218a46 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/example_branch_key_id_supplier.py @@ -0,0 +1,61 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Example implementation of a branch key ID supplier. + +Used in the 'HierarchicalKeyringExample'. +In that example, we have a table where we distinguish multiple tenants +by a tenant ID that is stored in our partition attribute. +The expectation is that this does not produce a confused deputy +because the tenants are separated by partition. +In order to create a Hierarchical Keyring that is capable of encrypting or +decrypting data for either tenant, we implement this interface +to map the correct branch key ID to the correct tenant ID. +""" +from typing import Dict + +from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb.references import ( + IDynamoDbKeyBranchKeyIdSupplier, +) +from aws_dbesdk_dynamodb.structures.dynamodb import GetBranchKeyIdFromDdbKeyInput, GetBranchKeyIdFromDdbKeyOutput + + +class ExampleBranchKeyIdSupplier(IDynamoDbKeyBranchKeyIdSupplier): + """Example implementation of a branch key ID supplier.""" + + branch_key_id_for_tenant1: str + branch_key_id_for_tenant2: str + + def __init__(self, tenant1_id: str, tenant2_id: str): + """ + Initialize a branch key ID supplier. + + :param tenant1_id: Branch key ID for tenant 1 + :param tenant2_id: Branch key ID for tenant 2 + """ + self.branch_key_id_for_tenant1 = tenant1_id + self.branch_key_id_for_tenant2 = tenant2_id + + def get_branch_key_id_from_ddb_key(self, param: GetBranchKeyIdFromDdbKeyInput) -> GetBranchKeyIdFromDdbKeyOutput: + """ + Get branch key ID from the tenant ID in input's DDB key. + + :param param: Input containing DDB key + :return: Output containing branch key ID + :raises ValueError: If DDB key is invalid or contains invalid tenant ID + """ + key: Dict[str, Dict] = param.ddb_key + + if "partition_key" not in key: + raise ValueError("Item invalid, does not contain expected partition key attribute.") + + tenant_key_id = key["partition_key"]["S"] + + if tenant_key_id == "tenant1Id": + branch_key_id = self.branch_key_id_for_tenant1 + elif tenant_key_id == "tenant2Id": + branch_key_id = self.branch_key_id_for_tenant2 + else: + raise ValueError("Item does not contain valid tenant ID") + + return GetBranchKeyIdFromDdbKeyOutput(branch_key_id=branch_key_id) diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/hierarchical_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/hierarchical_keyring_example.py new file mode 100644 index 000000000..9c68125e6 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/hierarchical_keyring_example.py @@ -0,0 +1,229 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Example demonstrating DynamoDb Encryption using a Hierarchical Keyring. + +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 +""" + +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 ( + CacheTypeDefault, + 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, +) +from aws_dbesdk_dynamodb.structures.dynamodb import ( + CreateDynamoDbEncryptionBranchKeyIdSupplierInput, + DynamoDbTableEncryptionConfig, + DynamoDbTablesEncryptionConfig, +) +from aws_dbesdk_dynamodb.structures.structured_encryption import ( + CryptoAction, +) + +from .example_branch_key_id_supplier import ExampleBranchKeyIdSupplier + + +def hierarchical_keyring_get_item_put_item( + 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. + + :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 + """ + # Initial KeyStore Setup: This example requires that you have already + # created your KeyStore, and have populated it with two new branch keys. + # See the "Create KeyStore Table Example" and "Create KeyStore Key Example" + # for an example of how to do this. + + # 1. Configure your KeyStore resource. + # This SHOULD be the same configuration that you used + # to initially create and populate your KeyStore. + keystore = KeyStore( + config=KeyStoreConfig( + ddb_client=boto3.client("dynamodb"), + ddb_table_name=keystore_table_name, + logical_key_store_name=logical_keystore_name, + kms_client=boto3.client("kms"), + kms_configuration=KMSConfigurationKmsKeyArn(kms_key_id), + ) + ) + + # 2. Create a Branch Key ID Supplier. See ExampleBranchKeyIdSupplier in this directory. + ddb_enc = DynamoDbEncryption(config=DynamoDbEncryptionConfig()) + branch_key_id_supplier = ddb_enc.create_dynamo_db_encryption_branch_key_id_supplier( + input=CreateDynamoDbEncryptionBranchKeyIdSupplierInput( + ddb_key_branch_key_id_supplier=ExampleBranchKeyIdSupplier(tenant1_branch_key_id, tenant2_branch_key_id) + ) + ).branch_key_id_supplier + + # 3. Create the Hierarchical Keyring, using the Branch Key ID Supplier above. + # With this configuration, the AWS SDK Client ultimately configured will be capable + # of encrypting or decrypting items for either tenant (assuming correct KMS access). + # If you want to restrict the client to only encrypt or decrypt for a single tenant, + # configure this Hierarchical Keyring using `.branch_key_id=tenant1_branch_key_id` instead + # of `.branch_key_id_supplier=branch_key_id_supplier`. + mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig()) + + keyring_input = CreateAwsKmsHierarchicalKeyringInput( + key_store=keystore, + branch_key_id_supplier=branch_key_id_supplier, + ttl_seconds=600, # This dictates how often we call back to KMS to authorize use of the branch keys + cache=CacheTypeDefault( # This dictates how many branch keys will be held locally + value=DefaultCache(entry_capacity=100) + ), + ) + + hierarchical_keyring = mat_prov.create_aws_kms_hierarchical_keyring(input=keyring_input) + + # 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 + "tenant_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. + 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=hierarchical_keyring, + 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_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!" 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.py new file mode 100644 index 000000000..3af215b1f --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_ecdh_keyring_example.py @@ -0,0 +1,451 @@ +# 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. + +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 pathlib + +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.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 + +EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME = "KmsEccKeyringKeyringExamplePublicKeySender.pem" +EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME = "KmsEccKeyringKeyringExamplePublicKeyRecipient.pem" + + +def kms_ecdh_keyring_get_item_put_item( + 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_get_item(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 EncryptedClient + ddb_client = boto3.client("dynamodb") + encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config) + + # Get the item back from our table using the client. + # The client will decrypt the item client-side using the ECDH keyring + # and return the original item. + key_to_get = {"partition_key": {"S": "awsKmsEcdhKeyringItem"}, "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 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 EncryptedClient + ddb_client = boto3.client("dynamodb") + encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config) + + # 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": "awsKmsEcdhKeyringItem"}, + "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 + + # 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": "awsKmsEcdhKeyringItem"}, "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 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_rsa_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_rsa_keyring_example.py new file mode 100644 index 000000000..dd18e9e48 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_rsa_keyring_example.py @@ -0,0 +1,230 @@ +# 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/mrk_discovery_multi_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/mrk_discovery_multi_keyring_example.py new file mode 100644 index 000000000..7d6c77357 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/mrk_discovery_multi_keyring_example.py @@ -0,0 +1,188 @@ +# 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. + +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.client import EncryptedClient +from aws_dbesdk_dynamodb.structures.dynamodb import ( + DynamoDbTableEncryptionConfig, + DynamoDbTablesEncryptionConfig, +) +from aws_dbesdk_dynamodb.structures.structured_encryption import ( + CryptoAction, +) + + +def multi_mrk_discovery_keyring_get_item_put_item( + ddb_table_name: str, key_arn: str, account_ids: List[str], regions: List[str] +): + """ + Demonstrate using a MRK discovery multi-keyring. + + :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 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 using the MRK multi-keyring. + item = { + "partition_key": {"S": "awsKmsMrkDiscoveryMultiKeyringItem"}, + "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. 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_client_for_decrypt = EncryptedClient(client=ddb_client, encryption_config=tables_config_for_decrypt) + + # 10. Get the item back from our table using the client. + # The client will retrieve encrypted items from the DDB table, then + # detect the KMS key that was used to encrypt their data keys. + # The client will make a request to KMS to decrypt with the encrypting KMS key. + # If the client has permission to decrypt with the KMS key, + # the client will decrypt the item client-side using the keyring + # and return the original item. + key_to_get = {"partition_key": {"S": "awsKmsMrkDiscoveryMultiKeyringItem"}, "sort_key": {"N": "0"}} + + get_response = encrypted_ddb_client_for_decrypt.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/mrk_multi_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/mrk_multi_keyring_example.py new file mode 100644 index 000000000..dbc774763 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/mrk_multi_keyring_example.py @@ -0,0 +1,245 @@ +# 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.client import EncryptedClient +from aws_dbesdk_dynamodb.structures.dynamodb import ( + DynamoDbTableEncryptionConfig, + DynamoDbTablesEncryptionConfig, +) +from aws_dbesdk_dynamodb.structures.structured_encryption import ( + CryptoAction, +) + + +def multi_mrk_keyring_get_item_put_item(ddb_table_name: str, mrk_key_arn: str, key_arn: str, mrk_replica_key_arn: str): + """ + Demonstrate using a MRK multi-keyring. + + :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 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 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": {"S": "awsKmsMrkMultiKeyringItem"}, + "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 client. + # 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": {"S": "awsKmsMrkMultiKeyringItem"}, "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!" + + # 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_ddb_client = EncryptedClient( + client=ddb_client, encryption_config=only_replica_key_tables_config + ) + + # 10. Get the item back from our table using the client 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_ddb_client.get_item( + TableName=ddb_table_name, 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"]["S"] == "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_ddb_client = EncryptedClient(client=ddb_client, encryption_config=only_srk_tables_config) + + # 13. Get the item back from our table using the client configured with the AWS KMS keyring. + # 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_ddb_client.get_item(TableName=ddb_table_name, 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"]["S"] == "encrypt and sign me!" diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/multi_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/multi_keyring_example.py new file mode 100644 index 000000000..5cb67df61 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/multi_keyring_example.py @@ -0,0 +1,211 @@ +# 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. + +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.client import EncryptedClient +from aws_dbesdk_dynamodb.structures.dynamodb import ( + DynamoDbTableEncryptionConfig, + DynamoDbTablesEncryptionConfig, +) +from aws_dbesdk_dynamodb.structures.structured_encryption import ( + CryptoAction, +) + + +def multi_keyring_get_item_put_item(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_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 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": {"S": "multiKeyringItem"}, + "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 + + # 9. Get the item back from our table using the above client. + # The client 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": {"S": "multiKeyringItem"}, "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!" + + # 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_ddb_client = EncryptedClient( + client=ddb_client, 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 client 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_ddb_client.get_item( + TableName=ddb_table_name, 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"]["S"] == "encrypt and sign me!" 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.py new file mode 100644 index 000000000..2b4993e26 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_aes_keyring_example.py @@ -0,0 +1,145 @@ +# 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. + +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_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, +) +from aws_dbesdk_dynamodb.structures.structured_encryption import ( + CryptoAction, +) + + +def raw_aes_keyring_get_item_put_item(ddb_table_name: str, aes_key_bytes: bytes): + """ + Demonstrate using a raw AES keyring. + + :param ddb_table_name: The name of the DynamoDB table + :param aes_key_bytes: The AES key bytes to use + """ + # 1. Create the keyring. + # The DynamoDb encryption client uses this to encrypt and decrypt items. + mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig()) + + 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=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_aes_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 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!" 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.py new file mode 100644 index 000000000..54f79a46d --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_ecdh_keyring_example.py @@ -0,0 +1,564 @@ +# 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. + +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 pathlib + +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.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 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 raw_ecdh_keyring_get_item_put_item(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(raw_ecdh_keyring, ddb_table_name) + + +def ephemeral_raw_ecdh_keyring_put_item(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(raw_ecdh_keyring, ddb_table_name) + + +def discovery_raw_ecdh_keyring_get_item(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(raw_ecdh_keyring, ddb_table_name) + + +def put_get_example_with_keyring(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 EncryptedClient + ddb_client = boto3.client("dynamodb") + encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config) + + # 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": "rawEcdhKeyringItem"}, + "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 + + # 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": "rawEcdhKeyringItem"}, "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 put_example_with_keyring(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 EncryptedClient + ddb_client = boto3.client("dynamodb") + encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config) + + # 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": "rawEcdhKeyringItem"}, + "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 + + +def get_example_with_keyring(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 EncryptedClient + ddb_client = boto3.client("dynamodb") + encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config) + + # 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": "rawEcdhKeyringItem"}, "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_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_rsa_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_rsa_keyring_example.py new file mode 100644 index 000000000..f0de43eee --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_rsa_keyring_example.py @@ -0,0 +1,245 @@ +# 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/shared_cache_across_hierarchical_keyrings_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/shared_cache_across_hierarchical_keyrings_example.py new file mode 100644 index 000000000..a7a2338e2 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/shared_cache_across_hierarchical_keyrings_example.py @@ -0,0 +1,352 @@ +# 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.client import EncryptedClient +from aws_dbesdk_dynamodb.structures.dynamodb import ( + DynamoDbTableEncryptionConfig, + DynamoDbTablesEncryptionConfig, +) +from aws_dbesdk_dynamodb.structures.structured_encryption import ( + CryptoAction, +) + + +def get_ddb_client( + 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 EncryptedClient + ddb_client = boto3.client("dynamodb") + encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config) + + return encrypted_ddb_client + + +def put_get_items(ddb_table_name: str, ddb_client: boto3.client): + """ + Put and get items using the given DynamoDB client. + + :param ddb_table_name: The name of the DynamoDB table + :param ddb_client: 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": {"S": "id"}, "sort_key": {"N": "0"}, "sensitive_data": {"S": "encrypt and sign me!"}} + + put_response = ddb_client.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 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": {"S": "id"}, "sort_key": {"N": "0"}} + + get_response = 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 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_client1 = get_ddb_client(ddb_table_name, hierarchical_keyring1, attribute_actions_on_encrypt) + + # 7. Encrypt Decrypt roundtrip with ddb_client1 + put_get_items(ddb_table_name, ddb_client1) + + # 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_client2 = get_ddb_client(ddb_table_name, hierarchical_keyring2, attribute_actions_on_encrypt) + + # 11. Encrypt Decrypt roundtrip with ddb_client2 + put_get_items(ddb_table_name, ddb_client2) diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/scan_error_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/scan_error_example.py new file mode 100644 index 000000000..b844b573a --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/src/scan_error_example.py @@ -0,0 +1,149 @@ +# 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. + +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 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 +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, +) +from aws_dbesdk_dynamodb.structures.structured_encryption import ( + CryptoAction, +) + + +def print_exception(e: Exception, indent: str = ""): + """ + Print exception and any nested CollectionOfErrors. + + :param e: Exception to print + :param indent: Indentation string for nested errors + """ + print(indent + str(e), file=sys.stderr) + if isinstance(e.__cause__, CollectionOfErrors): + print(indent + str(e.__cause__), file=sys.stderr) + for err in e.__cause__.list(): + print_exception(err, indent + " ") + elif isinstance(e, CollectionOfErrors): + for err in e.list(): + print_exception(err, indent + " ") + + +def scan_error(kms_key_id: str, ddb_table_name: str): + """ + Demonstrate handling scan errors. + + :param kms_key_id: The ARN of the KMS key to use + :param ddb_table_name: The name of the DynamoDB table + """ + # 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. + # We will use the `create_aws_kms_mrk_multi_keyring` method to create this keyring, + # as it will correctly handle both single region and Multi-Region KMS Keys. + mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig()) + + kms_keyring = mat_prov.create_aws_kms_mrk_multi_keyring( + input=CreateAwsKmsMrkMultiKeyringInput(generator=kms_key_id) + ) + + # 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 + "attribute1": CryptoAction.ENCRYPT_AND_SIGN, + "attribute2": CryptoAction.SIGN_ONLY, + ":attribute3": CryptoAction.DO_NOTHING, + } + + # 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 have designed our DynamoDb table such that any attribute name with + # the ":" prefix should be considered unauthenticated. + 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=kms_keyring, + allowed_unsigned_attribute_prefix=unsign_attr_prefix, + # Specifying an algorithm suite is not required, + # but is done here to demonstrate how to do so. + # We suggest using the + # `ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_ECDSA_P384_SYMSIG_HMAC_SHA384` suite, + # which includes AES-GCM with key derivation, signing, and key commitment. + # This is also the default algorithm suite if one is not specified in this config. + # For more information on supported algorithm suites, see: + # https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/supported-algorithms.html + algorithm_suite_id=DBEAlgorithmSuiteId.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_ECDSA_P384_SYMSIG_HMAC_SHA384, + ) + + 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) 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/cleanup.py b/Examples/runtimes/python/DynamoDBEncryption/test/cleanup.py new file mode 100644 index 000000000..4ae8a3014 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/cleanup.py @@ -0,0 +1,82 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Test cleanup utilities for DynamoDB Encryption SDK. + +This module provides utilities for cleaning up resources after running tests. + +WARNING: Please be careful. This is only a test utility and should NOT be used in production code. +It is specifically designed for cleaning up test resources after test execution. +- Running this code on production resources or any data you want to keep could result + in cryptographic shredding (permanent loss of access to encrypted data). +- Only use this on test resources that you are willing to permanently delete. +- Never run this against any production DynamoDB tables. Ensure you have backups + of any important data before running cleanup operations. +""" +import boto3 + +BRANCH_KEY_IDENTIFIER_FIELD = "branch-key-id" +TYPE_FIELD = "type" + + +def delete_branch_key( + identifier: str, + table_name: str, + ddb_client: boto3.client, +) -> bool: + """ + Delete all branch key items with the given identifier. + + Args: + identifier: Branch key identifier to delete + table_name: DynamoDB table name + ddb_client: DynamoDB client to use + + Returns: + True if all items were deleted, False if more than 100 items exist + + Raises: + ValueError: If an item is not a branch key + + """ + if ddb_client is None: + ddb_client = boto3.client("dynamodb") + + # Query for items with matching identifier + query_response = ddb_client.query( + TableName=table_name, + KeyConditionExpression="#pk = :pk", + ExpressionAttributeNames={"#pk": BRANCH_KEY_IDENTIFIER_FIELD}, + ExpressionAttributeValues={":pk": {"S": identifier}}, + ) + + items = query_response.get("Items", []) + if not items: + return True + + # Create delete requests for each item + delete_items = [] + for item in items: + if TYPE_FIELD not in item: + raise ValueError("Item is not a branch key") + + delete_item = { + "Delete": { + "Key": {BRANCH_KEY_IDENTIFIER_FIELD: {"S": identifier}, TYPE_FIELD: item[TYPE_FIELD]}, + "TableName": table_name, + } + } + delete_items.append(delete_item) + + if not delete_items: + return True + + # DynamoDB transactions are limited to 100 items + if len(delete_items) > 100: + delete_items = delete_items[:100] + + # Execute the delete transaction + ddb_client.transact_write_items(TransactItems=delete_items) + + # Return False if we had to truncate the deletion + return len(items) <= 100 diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/__init__.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/__init__.py new file mode 100644 index 000000000..fa977e22f --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/__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_hierarchical_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_hierarchical_keyring_example.py new file mode 100644 index 000000000..61cdb10a0 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_hierarchical_keyring_example.py @@ -0,0 +1,37 @@ +# 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 new file mode 100644 index 000000000..d54bd273e --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_kms_ecdh_keyring_example.py @@ -0,0 +1,40 @@ +# 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_kms_rsa_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_kms_rsa_keyring_example.py new file mode 100644 index 000000000..908531d5f --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_kms_rsa_keyring_example.py @@ -0,0 +1,27 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test for the KMS RSA keyring example.""" +import pytest + +from ...src.keyring.kms_rsa_keyring_example import ( + kms_rsa_keyring_example, + should_get_new_public_key, + write_public_key_pem_for_rsa_key, +) +from ..test_utils import ( + TEST_DDB_TABLE_NAME, + TEST_KMS_RSA_KEY_ID, +) + +pytestmark = [pytest.mark.examples] + + +def test_kms_rsa_keyring_example(): + """Test the KMS RSA keyring 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) 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 new file mode 100644 index 000000000..d2238bfa7 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_mrk_discovery_multi_keyring_example.py @@ -0,0 +1,22 @@ +# 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_mrk_multi_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_mrk_multi_keyring_example.py new file mode 100644 index 000000000..e63ccc323 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_mrk_multi_keyring_example.py @@ -0,0 +1,21 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test MRK multi-keyring example.""" +import pytest + +from ...src.keyring.mrk_multi_keyring_example import multi_mrk_keyring_get_item_put_item +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_mrk_multi_keyring_example(): + """Test mrk_multi_keyring_example.""" + multi_mrk_keyring_get_item_put_item( + 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_multi_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_multi_keyring_example.py new file mode 100644 index 000000000..0db6f3d80 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_multi_keyring_example.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.""" +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 new file mode 100644 index 000000000..93da5bddf --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_raw_aes_keyring_example.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 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_ecdh_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_raw_ecdh_keyring_example.py new file mode 100644 index 000000000..6a7676d89 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_raw_ecdh_keyring_example.py @@ -0,0 +1,74 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test raw ECDH keyring examples.""" +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, + 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 + +pytestmark = [pytest.mark.examples] + + +def test_static_raw_ecdh_keyring_example(): + """Test raw_ecdh_keyring_example 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 + # 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(TEST_DDB_TABLE_NAME, ECDHCurveSpec.ECC_NIST_P256) + + +def test_ephemeral_raw_ecdh_keyring_example(): + """Test raw_ecdh_keyring_example 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 + # 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(TEST_DDB_TABLE_NAME, ECDHCurveSpec.ECC_NIST_P256) + + +def test_discovery_raw_ecdh_keyring_example(): + """Test raw_ecdh_keyring_example 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 + # 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, 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. + + # 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) + + # 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) 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 new file mode 100644 index 000000000..590a7948e --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_raw_rsa_keyring_example.py @@ -0,0 +1,25 @@ +# 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/keyring/test_shared_cache_across_hierarchical_keyrings_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_shared_cache_across_hierarchical_keyrings_example.py new file mode 100644 index 000000000..2cfc10fe7 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_shared_cache_across_hierarchical_keyrings_example.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 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/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_create_keystore_key_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/test_create_keystore_key_example.py new file mode 100644 index 000000000..10a3c2cad --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/test_create_keystore_key_example.py @@ -0,0 +1,20 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test create key store key example.""" +import pytest + +from ..src.create_keystore_key_example import keystore_create_key +from .cleanup import delete_branch_key +from .test_utils import TEST_KEYSTORE_KMS_KEY_ID, TEST_KEYSTORE_NAME, TEST_LOGICAL_KEYSTORE_NAME + +pytestmark = [pytest.mark.examples] + + +def test_create_keystore_key_example(): + """Test create_key_store_key_example.""" + key_id = keystore_create_key(TEST_KEYSTORE_NAME, TEST_LOGICAL_KEYSTORE_NAME, TEST_KEYSTORE_KMS_KEY_ID) + + assert key_id is not None + + # Cleanup Branch Key + delete_branch_key(key_id, TEST_KEYSTORE_NAME, None) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/test_create_keystore_table_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/test_create_keystore_table_example.py new file mode 100644 index 000000000..fd030a0f7 --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/test_create_keystore_table_example.py @@ -0,0 +1,14 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test create key store table example.""" +import pytest + +from ..src.create_keystore_table_example import keystore_create_table +from .test_utils import TEST_KEYSTORE_KMS_KEY_ID, TEST_KEYSTORE_NAME, TEST_LOGICAL_KEYSTORE_NAME + +pytestmark = [pytest.mark.examples] + + +def test_create_keystore_table_example(): + """Test create_key_store_table_example.""" + keystore_create_table(TEST_KEYSTORE_NAME, TEST_LOGICAL_KEYSTORE_NAME, TEST_KEYSTORE_KMS_KEY_ID) 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, + ) diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/test_scan_error_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/test_scan_error_example.py new file mode 100644 index 000000000..ee0c3710d --- /dev/null +++ b/Examples/runtimes/python/DynamoDBEncryption/test/test_scan_error_example.py @@ -0,0 +1,14 @@ +# 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 import scan_error +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)