diff --git a/examples/README.md b/examples/README.md index f370f7407..592872232 100644 --- a/examples/README.md +++ b/examples/README.md @@ -14,14 +14,45 @@ and streaming APIs. You can find examples that demonstrate these APIs in the [`examples/src/`](./src) directory. +* [How to encrypt and decrypt](./src/onestep_defaults.py) +* [How to change the algorithm suite](./src/onestep_unsigned.py) +* [How to encrypt and decrypt data streams in memory](./src/in_memory_streaming_defaults.py) +* [How to encrypt and decrypt data streamed between files](./src/file_streaming_defaults.py) + ## Configuration -To use the library APIs, +To use the encryption and decryption APIs, you need to describe how you want the library to protect your data keys. -You can do this using -[keyrings][#keyrings] or [cryptographic materials managers][#cryptographic-materials-managers], -or using [master key providers][#master-key-providers]. -These examples will show you how. +You can do this by configuring +[keyrings](#keyrings) or [cryptographic materials managers](#cryptographic-materials-managers), +or by configuring [master key providers](#master-key-providers). +These examples will show you how to use the configuration tools that we include for you +and how to create some of your own. +We start with AWS KMS examples, then show how to use other wrapping keys. + +* Using AWS Key Management Service (AWS KMS) + * How to use one AWS KMS CMK + * [with keyrings](./src/keyring/aws_kms/single_cmk.py) + * How to use multiple AWS KMS CMKs in different regions + * [with keyrings](./src/keyring/aws_kms/multiple_regions.py) + * How to decrypt when you don't know the CMK + * [with keyrings](./src/keyring/aws_kms/discovery_decrypt.py) + * How to decrypt within a region + * [with keyrings](./src/keyring/aws_kms/discovery_decrypt_in_region_only.py) + * How to decrypt with a preferred region but failover to others + * [with keyrings](./src/keyring/aws_kms/discovery_decrypt_with_preferred_regions.py) +* Using raw wrapping keys + * How to use a raw AES wrapping key + * [with keyrings](./src/keyring/raw_aes/raw_aes.py) + * How to use a raw RSA wrapping key + * [with keyrings](./src/keyring/raw_rsa/private_key_only.py) + * How to use a raw RSA wrapping key when the key is PEM or DER encoded + * [with keyrings](./src/keyring/raw_rsa/private_key_only_from_pem.py) + * How to encrypt with a raw RSA public key wrapping key without access to the private key + * [with keyrings](./src/keyring/raw_rsa/public_private_key_separate.py) +* Combining wrapping keys + * How to combine AWS KMS with an offline escrow key + * [with keyrings](./src/keyring/multi/aws_kms_with_escrow.py) ### Keyrings @@ -56,7 +87,8 @@ you can find these examples in [`examples/src/master_key_provider`](./src/master ## Legacy -This section includes older examples, including examples of using master keys and master key providers in Java and Python. +This section includes older examples, +including examples of using master keys and master key providers. You can use them as a reference, but we recommend looking at the newer examples, which explain the preferred ways of using this library. You can find these examples in [`examples/src/legacy`](./src/legacy). diff --git a/examples/src/file_streaming_defaults.py b/examples/src/file_streaming_defaults.py index 983bfc90d..84bba1d31 100644 --- a/examples/src/file_streaming_defaults.py +++ b/examples/src/file_streaming_defaults.py @@ -54,15 +54,15 @@ def run(aws_kms_cmk, source_plaintext_filename): for segment in encryptor: ciphertext.write(segment) - # Verify that the ciphertext and plaintext are different. + # Demonstrate that the ciphertext and plaintext are different. assert not filecmp.cmp(source_plaintext_filename, ciphertext_filename) # Open the files you want to work with. with open(ciphertext_filename, "rb") as ciphertext, open(decrypted_filename, "wb") as decrypted: # Decrypt your encrypted data using the same keyring you used on encrypt. # - # We do not need to specify the encryption context on decrypt - # because the message header includes the encryption context. + # You do not need to specify the encryption context on decrypt + # because the header of the encrypted message includes the encryption context. with aws_encryption_sdk.stream(mode="decrypt", source=ciphertext, keyring=keyring) as decryptor: # Check the encryption context in the header before we start decrypting. # @@ -78,5 +78,5 @@ def run(aws_kms_cmk, source_plaintext_filename): for segment in decryptor: decrypted.write(segment) - # Verify that the decrypted plaintext is identical to the original plaintext. + # Demonstrate that the decrypted plaintext is identical to the original plaintext. assert filecmp.cmp(source_plaintext_filename, decrypted_filename) diff --git a/examples/src/in_memory_streaming_defaults.py b/examples/src/in_memory_streaming_defaults.py index 1a2824b94..984848bce 100644 --- a/examples/src/in_memory_streaming_defaults.py +++ b/examples/src/in_memory_streaming_defaults.py @@ -9,7 +9,7 @@ In this example, we use an AWS KMS customer master key (CMK), but you can use other key management options with the AWS Encryption SDK. For examples that demonstrate how to use other key management configurations, -see the ``keyring`` and ``mater_key_provider`` directories. +see the ``keyring`` and ``master_key_provider`` directories. """ import io @@ -49,7 +49,7 @@ def run(aws_kms_cmk, source_plaintext): for segment in encryptor: ciphertext.write(segment) - # Verify that the ciphertext and plaintext are different. + # Demonstrate that the ciphertext and plaintext are different. assert ciphertext.getvalue() != source_plaintext # Reset the ciphertext stream position so that we can read from the beginning. @@ -57,8 +57,8 @@ def run(aws_kms_cmk, source_plaintext): # Decrypt your encrypted data using the same keyring you used on encrypt. # - # We do not need to specify the encryption context on decrypt - # because the header message includes the encryption context. + # You do not need to specify the encryption context on decrypt + # because the header of the encrypted message includes the encryption context. decrypted = io.BytesIO() with aws_encryption_sdk.stream(mode="decrypt", source=ciphertext, keyring=keyring) as decryptor: # Check the encryption context in the header before we start decrypting. @@ -75,5 +75,5 @@ def run(aws_kms_cmk, source_plaintext): for segment in decryptor: decrypted.write(segment) - # Verify that the decrypted plaintext is identical to the original plaintext. + # Demonstrate that the decrypted plaintext is identical to the original plaintext. assert decrypted.getvalue() == source_plaintext diff --git a/examples/src/keyring/__init__.py b/examples/src/keyring/__init__.py new file mode 100644 index 000000000..c718f08e8 --- /dev/null +++ b/examples/src/keyring/__init__.py @@ -0,0 +1,7 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Keyring examples. + +These examples show how to use keyrings. +""" diff --git a/examples/src/keyring/aws_kms/__init__.py b/examples/src/keyring/aws_kms/__init__.py new file mode 100644 index 000000000..e9d9452c8 --- /dev/null +++ b/examples/src/keyring/aws_kms/__init__.py @@ -0,0 +1,7 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +AWS KMS keyring examples. + +These examples show how to use the KMS keyring. +""" diff --git a/examples/src/keyring/aws_kms/custom_client_supplier.py b/examples/src/keyring/aws_kms/custom_client_supplier.py new file mode 100644 index 000000000..159f8a2ab --- /dev/null +++ b/examples/src/keyring/aws_kms/custom_client_supplier.py @@ -0,0 +1,115 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +By default, the KMS keyring uses a client supplier that +supplies a client with the same configuration for every region. +If you need different behavior, you can write your own client supplier. + +You might use this +if you need different credentials in different AWS regions. +This might be because you are crossing partitions (ex: ``aws`` and ``aws-cn``) +or if you are working with regions that have separate authentication silos +like ``ap-east-1`` and ``me-south-1``. + +This example shows how to create a client supplier +that will supply KMS clients with valid credentials for the target region +even when working with regions that need different credentials. + +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-kms-keyring + +For an example of how to use the KMS keyring with CMKs in multiple regions, +see the ``keyring/aws_kms/multiple_regions`` example. + +For another example of how to use the KMS keyring with a custom client configuration, +see the ``keyring/aws_kms/custom_kms_client_config`` example. + +For examples of how to use the KMS keyring in discovery mode on decrypt, +see the ``keyring/aws_kms/discovery_decrypt``, +``keyring/aws_kms/discovery_decrypt_in_region_only``, +and ``keyring/aws_kms/discovery_decrypt_with_preferred_region`` examples. +""" +from botocore.client import BaseClient +from botocore.session import Session + +import aws_encryption_sdk +from aws_encryption_sdk.keyrings.aws_kms import KmsKeyring +from aws_encryption_sdk.keyrings.aws_kms.client_suppliers import ClientSupplier, DefaultClientSupplier + +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Union # noqa pylint: disable=unused-import +except ImportError: # pragma: no cover + # We only need these imports when running the mypy checks + pass + + +class MultiPartitionClientSupplier(ClientSupplier): + """Client supplier that supplies clients across AWS partitions and identity silos.""" + + def __init__(self): + """Set up default client suppliers for identity silos.""" + self._china_supplier = DefaultClientSupplier(botocore_session=Session(profile="china")) + self._middle_east_supplier = DefaultClientSupplier(botocore_session=Session(profile="middle-east")) + self._hong_kong_supplier = DefaultClientSupplier(botocore_session=Session(profile="hong-kong")) + self._default_supplier = DefaultClientSupplier() + + def __call__(self, region_name): + # type: (Union[None, str]) -> BaseClient + """Return a client for the requested region. + + :rtype: BaseClient + """ + if region_name.startswith("cn-"): + return self._china_supplier(region_name) + + if region_name.startswith("me-"): + return self._middle_east_supplier(region_name) + + if region_name == "ap-east-1": + return self._hong_kong_supplier(region_name) + + return self._default_supplier(region_name) + + +def run(aws_kms_cmk, source_plaintext): + # type: (str, bytes) -> None + """Demonstrate an encrypt/decrypt cycle using a KMS keyring with a custom client supplier. + + :param str aws_kms_cmk: The ARN of an AWS KMS CMK that protects data keys + :param bytes source_plaintext: Plaintext to encrypt + """ + # Prepare your encryption context. + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + encryption_context = { + "encryption": "context", + "is not": "secret", + "but adds": "useful metadata", + "that can help you": "be confident that", + "the data you are handling": "is what you think it is", + } + + # Create the keyring that determines how your data keys are protected. + keyring = KmsKeyring(generator_key_id=aws_kms_cmk, client_supplier=MultiPartitionClientSupplier()) + + # Encrypt your plaintext data. + ciphertext, _encrypt_header = aws_encryption_sdk.encrypt( + source=source_plaintext, encryption_context=encryption_context, keyring=keyring + ) + + # Demonstrate that the ciphertext and plaintext are different. + assert ciphertext != source_plaintext + + # Decrypt your encrypted data using the same keyring you used on encrypt. + # + # You do not need to specify the encryption context on decrypt + # because the header of the encrypted message includes the encryption context. + decrypted, decrypt_header = aws_encryption_sdk.decrypt(source=ciphertext, keyring=keyring) + + # Demonstrate that the decrypted plaintext is identical to the original plaintext. + assert decrypted == source_plaintext + + # Verify that the encryption context used in the decrypt operation includes + # the encryption context that you specified when encrypting. + # The AWS Encryption SDK can add pairs, so don't require an exact match. + # + # In production, always use a meaningful encryption context. + assert set(encryption_context.items()) <= set(decrypt_header.encryption_context.items()) diff --git a/examples/src/keyring/aws_kms/custom_kms_client_config.py b/examples/src/keyring/aws_kms/custom_kms_client_config.py new file mode 100644 index 000000000..2a8e7c759 --- /dev/null +++ b/examples/src/keyring/aws_kms/custom_kms_client_config.py @@ -0,0 +1,88 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +By default, the KMS keyring uses the default configurations +for all KMS clients and uses the default discoverable credentials. +If you need to change this configuration, +you can configure the client supplier. + +This example shows how to use custom-configured clients with the KMS keyring. + +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-kms-keyring + +For an example of how to use the KMS keyring with CMKs in multiple regions, +see the ``keyring/aws_kms/multiple_regions`` example. + +For another example of how to use the KMS keyring with custom client configuration, +see the ``keyring/aws_kms/custom_client_supplier`` example. + +For examples of how to use the KMS keyring in discovery mode on decrypt, +see the ``keyring/aws_kms/discovery_decrypt``, +``keyring/aws_kms/discovery_decrypt_in_region_only``, +and ``keyring/aws_kms/discovery_decrypt_with_preferred_region`` examples. +""" +from botocore.config import Config +from botocore.session import Session + +import aws_encryption_sdk +from aws_encryption_sdk.identifiers import USER_AGENT_SUFFIX +from aws_encryption_sdk.keyrings.aws_kms import KmsKeyring +from aws_encryption_sdk.keyrings.aws_kms.client_suppliers import DefaultClientSupplier + + +def run(aws_kms_cmk, source_plaintext): + # type: (str, bytes) -> None + """Demonstrate an encrypt/decrypt cycle using a KMS keyring with custom KMS client configuration. + + :param str aws_kms_cmk: The ARN of an AWS KMS CMK that protects data keys + :param bytes source_plaintext: Plaintext to encrypt + """ + # Prepare your encryption context. + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + encryption_context = { + "encryption": "context", + "is not": "secret", + "but adds": "useful metadata", + "that can help you": "be confident that", + "the data you are handling": "is what you think it is", + } + + # Prepare your custom configuration values. + # + # Set your custom connection timeout value. + # https://botocore.amazonaws.com/v1/documentation/api/latest/reference/config.html + custom_client_config = Config(connect_timeout=10.0, user_agent_extra=USER_AGENT_SUFFIX) + # For this example we will just use the default botocore session configuration + # but if you need to, you can set custom credentials in the botocore session. + custom_session = Session() + + # Use your custom configuration values to configure your client supplier. + client_supplier = DefaultClientSupplier(botocore_session=custom_session, client_config=custom_client_config) + + # Create the keyring that determines how your data keys are protected, + # providing the client supplier that you created. + keyring = KmsKeyring(generator_key_id=aws_kms_cmk, client_supplier=client_supplier) + + # Encrypt your plaintext data. + ciphertext, _encrypt_header = aws_encryption_sdk.encrypt( + source=source_plaintext, encryption_context=encryption_context, keyring=keyring + ) + + # Demonstrate that the ciphertext and plaintext are different. + assert ciphertext != source_plaintext + + # Decrypt your encrypted data using the same keyring you used on encrypt. + # + # You do not need to specify the encryption context on decrypt + # because the header of the encrypted message includes the encryption context. + decrypted, decrypt_header = aws_encryption_sdk.decrypt(source=ciphertext, keyring=keyring) + + # Demonstrate that the decrypted plaintext is identical to the original plaintext. + assert decrypted == source_plaintext + + # Verify that the encryption context used in the decrypt operation includes + # the encryption context that you specified when encrypting. + # The AWS Encryption SDK can add pairs, so don't require an exact match. + # + # In production, always use a meaningful encryption context. + assert set(encryption_context.items()) <= set(decrypt_header.encryption_context.items()) diff --git a/examples/src/keyring/aws_kms/discovery_decrypt.py b/examples/src/keyring/aws_kms/discovery_decrypt.py new file mode 100644 index 000000000..85f0e9c2c --- /dev/null +++ b/examples/src/keyring/aws_kms/discovery_decrypt.py @@ -0,0 +1,76 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +When you give the KMS keyring specific key IDs it will use those CMKs and nothing else. +This is true both on encrypt and on decrypt. +However, sometimes you need more flexibility on decrypt, +especially when you don't know which CMKs were used to encrypt a message. +To address this need, you can use a KMS discovery keyring. +The KMS discovery keyring does nothing on encrypt, +but attempts to decrypt *any* data keys that were encrypted under a KMS CMK. + +This example shows how to configure and use a KMS discovery keyring. + +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-kms-keyring + +For an example of how to use the KMS keyring with CMKs in multiple regions, +see the ``keyring/aws_kms/multiple_regions`` example. + +For examples of how to use the KMS keyring with custom client configurations, +see the ``keyring/aws_kms/custom_client_supplier`` +and ``keyring/aws_kms/custom_kms_client_config`` examples. + +For examples of how to use the KMS discovery keyring on decrypt, +see the ``keyring/aws_kms/discovery_decrypt_in_region_only`` +and ``keyring/aws_kms/discovery_decrypt_with_preferred_region`` examples. +""" +import aws_encryption_sdk +from aws_encryption_sdk.keyrings.aws_kms import KmsKeyring + + +def run(aws_kms_cmk, source_plaintext): + # type: (str, bytes) -> None + """Demonstrate configuring a KMS discovery keyring for decryption. + + :param str aws_kms_cmk: The ARN of an AWS KMS CMK that protects data keys + :param bytes source_plaintext: Plaintext to encrypt + """ + # Prepare your encryption context. + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + encryption_context = { + "encryption": "context", + "is not": "secret", + "but adds": "useful metadata", + "that can help you": "be confident that", + "the data you are handling": "is what you think it is", + } + + # Create the keyring that determines how your data keys are protected. + encrypt_keyring = KmsKeyring(generator_key_id=aws_kms_cmk) + + # Create a KMS discovery keyring to use on decrypt. + decrypt_keyring = KmsKeyring(is_discovery=True) + + # Encrypt your plaintext data. + ciphertext, _encrypt_header = aws_encryption_sdk.encrypt( + source=source_plaintext, encryption_context=encryption_context, keyring=encrypt_keyring + ) + + # Demonstrate that the ciphertext and plaintext are different. + assert ciphertext != source_plaintext + + # Decrypt your encrypted data using the KMS discovery keyring. + # + # You do not need to specify the encryption context on decrypt + # because the header of the encrypted message includes the encryption context. + decrypted, decrypt_header = aws_encryption_sdk.decrypt(source=ciphertext, keyring=decrypt_keyring) + + # Demonstrate that the decrypted plaintext is identical to the original plaintext. + assert decrypted == source_plaintext + + # Verify that the encryption context used in the decrypt operation includes + # the encryption context that you specified when encrypting. + # The AWS Encryption SDK can add pairs, so don't require an exact match. + # + # In production, always use a meaningful encryption context. + assert set(encryption_context.items()) <= set(decrypt_header.encryption_context.items()) diff --git a/examples/src/keyring/aws_kms/discovery_decrypt_in_region_only.py b/examples/src/keyring/aws_kms/discovery_decrypt_in_region_only.py new file mode 100644 index 000000000..326f512ec --- /dev/null +++ b/examples/src/keyring/aws_kms/discovery_decrypt_in_region_only.py @@ -0,0 +1,89 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +When you give the KMS keyring specific key IDs it will use those CMKs and nothing else. +This is true both on encrypt and on decrypt. +However, sometimes you need more flexibility on decrypt, +especially when you don't know which CMKs were used to encrypt a message. +To address this need, you can use a KMS discovery keyring. +The KMS discovery keyring does nothing on encrypt, +but attempts to decrypt *any* data keys that were encrypted under a KMS CMK. + +However, sometimes you need to be a *bit* more restrictive than that. +To address this need, you can use a client supplier that restricts the regions a KMS keyring can talk to. + +This example shows how to configure and use a KMS regional discovery keyring that is restricted to one region. + +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-kms-keyring + +For an example of how to use the KMS keyring with CMKs in multiple regions, +see the ``keyring/aws_kms/multiple_regions`` example. + +For examples of how to use the KMS keyring with custom client configurations, +see the ``keyring/aws_kms/custom_client_supplier`` +and ``keyring/aws_kms/custom_kms_client_config`` examples. + +For examples of how to use the KMS discovery keyring on decrypt, +see the ``keyring/aws_kms/discovery_decrypt`` +and ``keyring/aws_kms/discovery_decrypt_with_preferred_region`` examples. +""" +import aws_encryption_sdk +from aws_encryption_sdk.keyrings.aws_kms import KmsKeyring +from aws_encryption_sdk.keyrings.aws_kms.client_suppliers import AllowRegionsClientSupplier + + +def run(aws_kms_cmk, source_plaintext): + # type: (str, bytes) -> None + """Demonstrate configuring a KMS discovery keyring to only work within a single region. + + :param str aws_kms_cmk: The ARN of an AWS KMS CMK that protects data keys + :param bytes source_plaintext: Plaintext to encrypt + """ + # Prepare your encryption context. + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + encryption_context = { + "encryption": "context", + "is not": "secret", + "but adds": "useful metadata", + "that can help you": "be confident that", + "the data you are handling": "is what you think it is", + } + + # Create the keyring that determines how your data keys are protected. + encrypt_keyring = KmsKeyring(generator_key_id=aws_kms_cmk) + + # Extract the region from the CMK ARN. + decrypt_region = aws_kms_cmk.split(":", 4)[3] + + # Create the KMS discovery keyring that we will use on decrypt. + # + # The client supplier that we specify here will only supply clients for the specified region. + # The keyring only attempts to decrypt data keys if it can get a client for that region, + # so this keyring will now ignore any data keys that were encrypted under a CMK in another region. + decrypt_keyring = KmsKeyring( + is_discovery=True, client_supplier=AllowRegionsClientSupplier(allowed_regions=[decrypt_region]) + ) + + # Encrypt your plaintext data. + ciphertext, _encrypt_header = aws_encryption_sdk.encrypt( + source=source_plaintext, encryption_context=encryption_context, keyring=encrypt_keyring + ) + + # Demonstrate that the ciphertext and plaintext are different. + assert ciphertext != source_plaintext + + # Decrypt your encrypted data using the KMS discovery keyring. + # + # You do not need to specify the encryption context on decrypt + # because the header of the encrypted message includes the encryption context. + decrypted, decrypt_header = aws_encryption_sdk.decrypt(source=ciphertext, keyring=decrypt_keyring) + + # Demonstrate that the decrypted plaintext is identical to the original plaintext. + assert decrypted == source_plaintext + + # Verify that the encryption context used in the decrypt operation includes + # the encryption context that you specified when encrypting. + # The AWS Encryption SDK can add pairs, so don't require an exact match. + # + # In production, always use a meaningful encryption context. + assert set(encryption_context.items()) <= set(decrypt_header.encryption_context.items()) diff --git a/examples/src/keyring/aws_kms/discovery_decrypt_with_preferred_regions.py b/examples/src/keyring/aws_kms/discovery_decrypt_with_preferred_regions.py new file mode 100644 index 000000000..ebe8a4953 --- /dev/null +++ b/examples/src/keyring/aws_kms/discovery_decrypt_with_preferred_regions.py @@ -0,0 +1,110 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +When you give the KMS keyring specific key IDs it will use those CMKs and nothing else. +This is true both on encrypt and on decrypt. +However, sometimes you need more flexibility on decrypt, +especially when you don't know which CMKs were used to encrypt a message. +To address this need, you can use a KMS discovery keyring. +The KMS discovery keyring does nothing on encrypt, +but attempts to decrypt *any* data keys that were encrypted under a KMS CMK. + +However, sometimes you need to be a *bit* more restrictive than that. +To address this need, you can use a client supplier to restrict what regions a KMS keyring can talk to. + +A more complex but more common use-case is that you would *prefer* to stay within a region, +but you would rather make calls to other regions than fail to decrypt the message. +In this case, you want a keyring that will try to decrypt data keys in this region first, +then try other regions. + +This example shows how to configure and use a multi-keyring with the KMS keyring +to prefer the current AWS region while also failing over to other AWS regions. + +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-kms-keyring + +For an example of how to use the KMS keyring with CMKs in multiple regions, +see the ``keyring/aws_kms/multiple_regions`` example. + +For examples of how to use the KMS keyring with custom client configurations, +see the ``keyring/aws_kms/custom_client_supplier`` +and ``keyring/aws_kms/custom_kms_client_config`` examples. + +For examples of how to use the KMS discovery keyring on decrypt, +see the ``keyring/aws_kms/discovery_decrypt`` +and ``keyring/aws_kms/discovery_decrypt_in_region_only`` examples. +""" +from boto3.session import Session + +import aws_encryption_sdk +from aws_encryption_sdk.keyrings.aws_kms import KmsKeyring +from aws_encryption_sdk.keyrings.aws_kms.client_suppliers import AllowRegionsClientSupplier, DenyRegionsClientSupplier +from aws_encryption_sdk.keyrings.multi import MultiKeyring + + +def run(aws_kms_cmk, source_plaintext): + # type: (str, bytes) -> None + """Demonstrate configuring a KMS discovery-like keyring a particular AWS region and failover to others. + + :param str aws_kms_cmk: The ARN of an AWS KMS CMK that protects data keys + :param bytes source_plaintext: Plaintext to encrypt + """ + # Prepare your encryption context. + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + encryption_context = { + "encryption": "context", + "is not": "secret", + "but adds": "useful metadata", + "that can help you": "be confident that", + "the data you are handling": "is what you think it is", + } + + # Create the keyring that determines how your data keys are protected. + encrypt_keyring = KmsKeyring(generator_key_id=aws_kms_cmk) + + # To create our decrypt keyring, we need to know our current default AWS region. + # + # Create a throw-away boto3 session to discover the default region. + local_region = Session().region_name + + # Now, use that region name to create two KMS discovery keyrings: + # + # One that only works in the local region + local_region_decrypt_keyring = KmsKeyring( + is_discovery=True, client_supplier=AllowRegionsClientSupplier(allowed_regions=[local_region]) + ) + # and one that will work in any other region but NOT the local region. + other_regions_decrypt_keyring = KmsKeyring( + is_discovery=True, client_supplier=DenyRegionsClientSupplier(denied_regions=[local_region]) + ) + + # Finally, combine those two keyrings into a multi-keyring. + # + # The multi-keyring steps through its member keyrings in the order that you provide them, + # attempting to decrypt every encrypted data key with each keyring before moving on to the next keyring. + # Because of this, other_regions_decrypt_keyring will not be called + # unless local_region_decrypt_keyring fails to decrypt every encrypted data key. + decrypt_keyring = MultiKeyring(children=[local_region_decrypt_keyring, other_regions_decrypt_keyring]) + + # Encrypt your plaintext data. + ciphertext, _encrypt_header = aws_encryption_sdk.encrypt( + source=source_plaintext, encryption_context=encryption_context, keyring=encrypt_keyring + ) + + # Demonstrate that the ciphertext and plaintext are different. + assert ciphertext != source_plaintext + + # Decrypt your encrypted data using the multi-keyring. + # + # You do not need to specify the encryption context on decrypt + # because the header of the encrypted message includes the encryption context. + decrypted, decrypt_header = aws_encryption_sdk.decrypt(source=ciphertext, keyring=decrypt_keyring) + + # Demonstrate that the decrypted plaintext is identical to the original plaintext. + assert decrypted == source_plaintext + + # Verify that the encryption context used in the decrypt operation includes + # the encryption context that you specified when encrypting. + # The AWS Encryption SDK can add pairs, so don't require an exact match. + # + # In production, always use a meaningful encryption context. + assert set(encryption_context.items()) <= set(decrypt_header.encryption_context.items()) diff --git a/examples/src/keyring/aws_kms/multiple_regions.py b/examples/src/keyring/aws_kms/multiple_regions.py new file mode 100644 index 000000000..f466796c3 --- /dev/null +++ b/examples/src/keyring/aws_kms/multiple_regions.py @@ -0,0 +1,93 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +This example shows how to configure and use a KMS keyring with with CMKs in multiple regions. + +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-kms-keyring + +For an example of how to use the KMS keyring with a single CMK, +see the ``keyring/aws_kms/single_cmk`` example. + +For examples of how to use the KMS keyring with custom client configurations, +see the ``keyring/aws_kms/custom_client_supplier`` +and ``keyring/aws_kms/custom_kms_client_config`` examples. + +For examples of how to use the KMS keyring in discovery mode on decrypt, +see the ``keyring/aws_kms/discovery_decrypt``, +``keyring/aws_kms/discovery_decrypt_in_region_only``, +and ``keyring/aws_kms/discovery_decrypt_with_preferred_region`` examples. +""" +import aws_encryption_sdk +from aws_encryption_sdk.keyrings.aws_kms import KmsKeyring + +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Sequence # noqa pylint: disable=unused-import +except ImportError: # pragma: no cover + # We only actually need these imports when running the mypy checks + pass + + +def run(aws_kms_generator_cmk, aws_kms_additional_cmks, source_plaintext): + # type: (str, Sequence[str], bytes) -> None + """Demonstrate an encrypt/decrypt cycle using a KMS keyring with CMKs in multiple regions. + + :param str aws_kms_generator_cmk: The ARN of the primary AWS KMS CMK + :param List[str] aws_kms_additional_cmks: Additional ARNs of secondary KMS CMKs + :param bytes source_plaintext: Plaintext to encrypt + """ + # Prepare your encryption context. + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + encryption_context = { + "encryption": "context", + "is not": "secret", + "but adds": "useful metadata", + "that can help you": "be confident that", + "the data you are handling": "is what you think it is", + } + + # Create the keyring that will encrypt your data keys under all requested CMKs. + many_cmks_keyring = KmsKeyring(generator_key_id=aws_kms_generator_cmk, child_key_ids=aws_kms_additional_cmks) + + # Create keyrings that each only use one of the CMKs. + # We will use these later to demonstrate that any of the CMKs can be used to decrypt the message. + # + # We provide these in "child_key_ids" rather than "generator_key_id" + # so that these keyrings cannot be used to generate a new data key. + # We will only be using them on decrypt. + single_cmk_keyring_that_generated = KmsKeyring(child_key_ids=[aws_kms_generator_cmk]) + single_cmk_keyring_that_encrypted = KmsKeyring(child_key_ids=[aws_kms_additional_cmks[0]]) + + # Encrypt your plaintext data using the keyring that uses all requests CMKs. + ciphertext, encrypt_header = aws_encryption_sdk.encrypt( + source=source_plaintext, encryption_context=encryption_context, keyring=many_cmks_keyring + ) + + # Verify that the header contains the expected number of encrypted data keys (EDKs). + # It should contain one EDK for each CMK. + assert len(encrypt_header.encrypted_data_keys) == len(aws_kms_additional_cmks) + 1 + + # Demonstrate that the ciphertext and plaintext are different. + assert ciphertext != source_plaintext + + # Decrypt your encrypted data separately using the single-CMK keyrings. + # + # You do not need to specify the encryption context on decrypt + # because the header of the encrypted message includes the encryption context. + decrypted_1, decrypt_header_1 = aws_encryption_sdk.decrypt( + source=ciphertext, keyring=single_cmk_keyring_that_generated + ) + decrypted_2, decrypt_header_2 = aws_encryption_sdk.decrypt( + source=ciphertext, keyring=single_cmk_keyring_that_encrypted + ) + + # Demonstrate that the decrypted plaintext is identical to the original plaintext. + assert decrypted_1 == source_plaintext + assert decrypted_2 == source_plaintext + + # Verify that the encryption context used in the decrypt operation includes + # the encryption context that you specified when encrypting. + # The AWS Encryption SDK can add pairs, so don't require an exact match. + # + # In production, always use a meaningful encryption context. + assert set(encryption_context.items()) <= set(decrypt_header_1.encryption_context.items()) + assert set(encryption_context.items()) <= set(decrypt_header_2.encryption_context.items()) diff --git a/examples/src/keyring/aws_kms/single_cmk.py b/examples/src/keyring/aws_kms/single_cmk.py new file mode 100644 index 000000000..b186f5056 --- /dev/null +++ b/examples/src/keyring/aws_kms/single_cmk.py @@ -0,0 +1,66 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +This example shows how to configure and use a KMS keyring with a single KMS CMK. + +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-kms-keyring + +For an example of how to use the KMS keyring with CMKs in multiple regions, +see the ``keyring/aws_kms/multiple_regions`` example. + +For examples of how to use the KMS keyring with custom client configurations, +see the ``keyring/aws_kms/custom_client_supplier`` +and ``keyring/aws_kms/custom_kms_client_config`` examples. + +For examples of how to use the KMS keyring in discovery mode on decrypt, +see the ``keyring/aws_kms/discovery_decrypt``, +``keyring/aws_kms/discovery_decrypt_in_region_only``, +and ``keyring/aws_kms/discovery_decrypt_with_preferred_region`` examples. +""" +import aws_encryption_sdk +from aws_encryption_sdk.keyrings.aws_kms import KmsKeyring + + +def run(aws_kms_cmk, source_plaintext): + # type: (str, bytes) -> None + """Demonstrate an encrypt/decrypt cycle using a KMS keyring with a single CMK. + + :param str aws_kms_cmk: The ARN of an AWS KMS CMK that protects data keys + :param bytes source_plaintext: Plaintext to encrypt + """ + # Prepare your encryption context. + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + encryption_context = { + "encryption": "context", + "is not": "secret", + "but adds": "useful metadata", + "that can help you": "be confident that", + "the data you are handling": "is what you think it is", + } + + # Create the keyring that determines how your data keys are protected. + keyring = KmsKeyring(generator_key_id=aws_kms_cmk) + + # Encrypt your plaintext data. + ciphertext, _encrypt_header = aws_encryption_sdk.encrypt( + source=source_plaintext, encryption_context=encryption_context, keyring=keyring + ) + + # Demonstrate that the ciphertext and plaintext are different. + assert ciphertext != source_plaintext + + # Decrypt your encrypted data using the same keyring you used on encrypt. + # + # You do not need to specify the encryption context on decrypt + # because the header of the encrypted message includes the encryption context. + decrypted, decrypt_header = aws_encryption_sdk.decrypt(source=ciphertext, keyring=keyring) + + # Demonstrate that the decrypted plaintext is identical to the original plaintext. + assert decrypted == source_plaintext + + # Verify that the encryption context used in the decrypt operation includes + # the encryption context that you specified when encrypting. + # The AWS Encryption SDK can add pairs, so don't require an exact match. + # + # In production, always use a meaningful encryption context. + assert set(encryption_context.items()) <= set(decrypt_header.encryption_context.items()) diff --git a/examples/src/keyring/multi/__init__.py b/examples/src/keyring/multi/__init__.py new file mode 100644 index 000000000..e5703355a --- /dev/null +++ b/examples/src/keyring/multi/__init__.py @@ -0,0 +1,7 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Multi-keyring examples. + +These examples show how to use the multi-keyring. +""" diff --git a/examples/src/keyring/multi/aws_kms_with_escrow.py b/examples/src/keyring/multi/aws_kms_with_escrow.py new file mode 100644 index 000000000..dac35b9d9 --- /dev/null +++ b/examples/src/keyring/multi/aws_kms_with_escrow.py @@ -0,0 +1,128 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +One use-case that we have seen customers need is +the ability to enjoy the benefits of AWS KMS during normal operation +but retain the ability to decrypt encrypted messages without access to AWS KMS. +This example shows how you can use the multi-keyring to achieve this +by combining a KMS keyring with a raw RSA keyring. + +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-multi-keyring + +For more examples of how to use the KMS keyring, see the ``keyring/aws_kms`` examples. + +For more examples of how to use the raw RSA keyring, see the ``keyring/raw_rsa`` examples. + +In this example we generate a RSA keypair +but in practice you would want to keep your private key in an HSM +or other key management system. + +In this example, we use the one-step encrypt and decrypt APIs. +""" +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.asymmetric import rsa + +import aws_encryption_sdk +from aws_encryption_sdk.identifiers import WrappingAlgorithm +from aws_encryption_sdk.keyrings.aws_kms import KmsKeyring +from aws_encryption_sdk.keyrings.multi import MultiKeyring +from aws_encryption_sdk.keyrings.raw import RawRSAKeyring + + +def run(aws_kms_cmk, source_plaintext): + # type: (str, bytes) -> None + """Demonstrate configuring a keyring to use an AWS KMS CMK and a RSA wrapping key. + + :param str aws_kms_cmk: The ARN of an AWS KMS CMK that protects data keys + :param bytes source_plaintext: Plaintext to encrypt + """ + # Prepare your encryption context. + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + encryption_context = { + "encryption": "context", + "is not": "secret", + "but adds": "useful metadata", + "that can help you": "be confident that", + "the data you are handling": "is what you think it is", + } + + # Generate an RSA private key to use with your keyring. + # In practice, you should get this key from a secure key management system such as an HSM. + # + # The National Institute of Standards and Technology (NIST) recommends a minimum of 2048-bit keys for RSA. + # https://www.nist.gov/publications/transitioning-use-cryptographic-algorithms-and-key-lengths + # + # Why did we use this public exponent? + # https://crypto.stanford.edu/~dabo/pubs/papers/RSA-survey.pdf + private_key = rsa.generate_private_key(public_exponent=65537, key_size=4096, backend=default_backend()) + + # Collect the public key from the private key. + public_key = private_key.public_key() + + # Create the encrypt keyring that only has access to the public key. + escrow_encrypt_keyring = RawRSAKeyring( + # The key namespace and key name are defined by you + # and are used by the raw RSA keyring + # to determine whether it should attempt to decrypt + # an encrypted data key. + # + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-raw-rsa-keyring + key_namespace="some managed raw keys", + key_name=b"my RSA wrapping key", + public_wrapping_key=public_key, + # The wrapping algorithm tells the raw RSA keyring + # how to use your wrapping key to encrypt data keys. + # + # We recommend using RSA_OAEP_SHA256_MGF1. + # You should not use RSA_PKCS1 unless you require it for backwards compatibility. + wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, + ) + + # Create the decrypt keyring that has access to the private key. + escrow_decrypt_keyring = RawRSAKeyring( + # The key namespace and key name MUST match the encrypt keyring. + key_namespace="some managed raw keys", + key_name=b"my RSA wrapping key", + private_wrapping_key=private_key, + # The wrapping algorithm MUST match the encrypt keyring. + wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, + ) + + # Create the KMS keyring that you will use for decryption during normal operations. + kms_keyring = KmsKeyring(generator_key_id=aws_kms_cmk) + + # Combine the KMS keyring and the escrow encrypt keyring using the multi-keyring. + encrypt_keyring = MultiKeyring(generator=kms_keyring, children=[escrow_encrypt_keyring]) + + # Encrypt your plaintext data using the multi-keyring. + ciphertext, encrypt_header = aws_encryption_sdk.encrypt( + source=source_plaintext, encryption_context=encryption_context, keyring=encrypt_keyring + ) + + # Verify that the header contains the expected number of encrypted data keys (EDKs). + # It should contain one EDK for KMS and one for the escrow key. + assert len(encrypt_header.encrypted_data_keys) == 2 + + # Demonstrate that the ciphertext and plaintext are different. + assert ciphertext != source_plaintext + + # Decrypt your encrypted data separately using the KMS keyring and the escrow decrypt keyring. + # + # You do not need to specify the encryption context on decrypt + # because the header of the encrypted message includes the encryption context. + decrypted_kms, decrypt_header_kms = aws_encryption_sdk.decrypt(source=ciphertext, keyring=kms_keyring) + decrypted_escrow, decrypt_header_escrow = aws_encryption_sdk.decrypt( + source=ciphertext, keyring=escrow_decrypt_keyring + ) + + # Demonstrate that the decrypted plaintext is identical to the original plaintext. + assert decrypted_kms == source_plaintext + assert decrypted_escrow == source_plaintext + + # Verify that the encryption context used in the decrypt operation includes + # the encryption context that you specified when encrypting. + # The AWS Encryption SDK can add pairs, so don't require an exact match. + # + # In production, always use a meaningful encryption context. + assert set(encryption_context.items()) <= set(decrypt_header_kms.encryption_context.items()) + assert set(encryption_context.items()) <= set(decrypt_header_escrow.encryption_context.items()) diff --git a/examples/src/keyring/raw_aes/__init__.py b/examples/src/keyring/raw_aes/__init__.py new file mode 100644 index 000000000..2159bf30d --- /dev/null +++ b/examples/src/keyring/raw_aes/__init__.py @@ -0,0 +1,7 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Raw AES keyring examples. + +These examples show how to use the raw AES keyring. +""" diff --git a/examples/src/keyring/raw_aes/raw_aes.py b/examples/src/keyring/raw_aes/raw_aes.py new file mode 100644 index 000000000..58aa697c9 --- /dev/null +++ b/examples/src/keyring/raw_aes/raw_aes.py @@ -0,0 +1,78 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +This examples shows how to configure and use a raw AES keyring. + +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-raw-aes-keyring + +In this example, we use the one-step encrypt and decrypt APIs. +""" +import os + +import aws_encryption_sdk +from aws_encryption_sdk.identifiers import WrappingAlgorithm +from aws_encryption_sdk.keyrings.raw import RawAESKeyring + + +def run(source_plaintext): + # type: (bytes) -> None + """Demonstrate an encrypt/decrypt cycle using a raw AES keyring. + + :param bytes source_plaintext: Plaintext to encrypt + """ + # Prepare your encryption context. + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + encryption_context = { + "encryption": "context", + "is not": "secret", + "but adds": "useful metadata", + "that can help you": "be confident that", + "the data you are handling": "is what you think it is", + } + + # Choose the wrapping algorithm for the keyring to use. + wrapping_algorithm = WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING + + # Generate an AES key to use with your keyring. + # The key size depends on the wrapping algorithm. + # + # In practice, you should get this key from a secure key management system such as an HSM. + key = os.urandom(wrapping_algorithm.algorithm.kdf_input_len) + + # Create the keyring that determines how your data keys are protected. + keyring = RawAESKeyring( + # The key namespace and key name are defined by you + # and are used by the raw RSA keyring + # to determine whether it should attempt to decrypt + # an encrypted data key. + # + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-raw-aes-keyring + key_namespace="some managed raw keys", + key_name=b"my AES wrapping key", + wrapping_key=key, + wrapping_algorithm=wrapping_algorithm, + ) + + # Encrypt your plaintext data. + ciphertext, _encrypt_header = aws_encryption_sdk.encrypt( + source=source_plaintext, encryption_context=encryption_context, keyring=keyring + ) + + # Demonstrate that the ciphertext and plaintext are different. + assert ciphertext != source_plaintext + + # Decrypt your encrypted data using the same keyring you used on encrypt. + # + # You do not need to specify the encryption context on decrypt + # because the header of the encrypted message includes the encryption context. + decrypted, decrypt_header = aws_encryption_sdk.decrypt(source=ciphertext, keyring=keyring) + + # Demonstrate that the decrypted plaintext is identical to the original plaintext. + assert decrypted == source_plaintext + + # Verify that the encryption context used in the decrypt operation includes + # the encryption context that you specified when encrypting. + # The AWS Encryption SDK can add pairs, so don't require an exact match. + # + # In production, always use a meaningful encryption context. + assert set(encryption_context.items()) <= set(decrypt_header.encryption_context.items()) diff --git a/examples/src/keyring/raw_rsa/__init__.py b/examples/src/keyring/raw_rsa/__init__.py new file mode 100644 index 000000000..742761bbe --- /dev/null +++ b/examples/src/keyring/raw_rsa/__init__.py @@ -0,0 +1,7 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Raw RSA keyring examples. + +These examples show how to use the raw RSA keyring. +""" diff --git a/examples/src/keyring/raw_rsa/private_key_only.py b/examples/src/keyring/raw_rsa/private_key_only.py new file mode 100644 index 000000000..db7705735 --- /dev/null +++ b/examples/src/keyring/raw_rsa/private_key_only.py @@ -0,0 +1,88 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +This examples shows how to configure and use a raw RSA keyring using a pre-loaded RSA private key. + +If your RSA key is in PEM or DER format, +see the ``keyring/raw_rsa/private_key_only_from_pem`` example. + +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-raw-rsa-keyring + +In this example, we use the one-step encrypt and decrypt APIs. +""" +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.asymmetric import rsa + +import aws_encryption_sdk +from aws_encryption_sdk.identifiers import WrappingAlgorithm +from aws_encryption_sdk.keyrings.raw import RawRSAKeyring + + +def run(source_plaintext): + # type: (bytes) -> None + """Demonstrate an encrypt/decrypt cycle using a raw RSA keyring. + + :param bytes source_plaintext: Plaintext to encrypt + """ + # Prepare your encryption context. + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + encryption_context = { + "encryption": "context", + "is not": "secret", + "but adds": "useful metadata", + "that can help you": "be confident that", + "the data you are handling": "is what you think it is", + } + + # Generate an RSA private key to use with your keyring. + # In practice, you should get this key from a secure key management system such as an HSM. + # + # The National Institute of Standards and Technology (NIST) recommends a minimum of 2048-bit keys for RSA. + # https://www.nist.gov/publications/transitioning-use-cryptographic-algorithms-and-key-lengths + # + # Why did we use this public exponent? + # https://crypto.stanford.edu/~dabo/pubs/papers/RSA-survey.pdf + private_key = rsa.generate_private_key(public_exponent=65537, key_size=4096, backend=default_backend()) + + # Create the keyring that determines how your data keys are protected. + keyring = RawRSAKeyring( + # The key namespace and key name are defined by you + # and are used by the raw RSA keyring + # to determine whether it should attempt to decrypt + # an encrypted data key. + # + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-raw-rsa-keyring + key_namespace="some managed raw keys", + key_name=b"my RSA wrapping key", + private_wrapping_key=private_key, + # The wrapping algorithm tells the raw RSA keyring + # how to use your wrapping key to encrypt data keys. + # + # We recommend using RSA_OAEP_SHA256_MGF1. + # You should not use RSA_PKCS1 unless you require it for backwards compatibility. + wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, + ) + + # Encrypt your plaintext data. + ciphertext, _encrypt_header = aws_encryption_sdk.encrypt( + source=source_plaintext, encryption_context=encryption_context, keyring=keyring + ) + + # Demonstrate that the ciphertext and plaintext are different. + assert ciphertext != source_plaintext + + # Decrypt your encrypted data using the same keyring you used on encrypt. + # + # You do not need to specify the encryption context on decrypt + # because the header of the encrypted message includes the encryption context. + decrypted, decrypt_header = aws_encryption_sdk.decrypt(source=ciphertext, keyring=keyring) + + # Demonstrate that the decrypted plaintext is identical to the original plaintext. + assert decrypted == source_plaintext + + # Verify that the encryption context used in the decrypt operation includes + # the encryption context that you specified when encrypting. + # The AWS Encryption SDK can add pairs, so don't require an exact match. + # + # In production, always use a meaningful encryption context. + assert set(encryption_context.items()) <= set(decrypt_header.encryption_context.items()) diff --git a/examples/src/keyring/raw_rsa/private_key_only_from_pem.py b/examples/src/keyring/raw_rsa/private_key_only_from_pem.py new file mode 100644 index 000000000..397a79274 --- /dev/null +++ b/examples/src/keyring/raw_rsa/private_key_only_from_pem.py @@ -0,0 +1,101 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +When you store RSA keys, you have to serialize them somehow. + +This example shows how to configure and use a raw RSA keyring using a PEM-encoded RSA private key. + +The most commonly used encodings for RSA keys tend to be PEM and DER. +The raw RSA keyring supports loading both public and private keys from these encodings. + +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-raw-rsa-keyring + +In this example, we use the one-step encrypt and decrypt APIs. +""" +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import rsa + +import aws_encryption_sdk +from aws_encryption_sdk.identifiers import WrappingAlgorithm +from aws_encryption_sdk.keyrings.raw import RawRSAKeyring + + +def run(source_plaintext): + # type: (bytes) -> None + """Demonstrate an encrypt/decrypt cycle using a raw RSA keyring loaded from a PEM-encoded key. + + :param bytes source_plaintext: Plaintext to encrypt + """ + # Prepare your encryption context. + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + encryption_context = { + "encryption": "context", + "is not": "secret", + "but adds": "useful metadata", + "that can help you": "be confident that", + "the data you are handling": "is what you think it is", + } + + # Generate an RSA private key to use with your keyring. + # In practice, you should get this key from a secure key management system such as an HSM. + # + # The National Institute of Standards and Technology (NIST) recommends a minimum of 2048-bit keys for RSA. + # https://www.nist.gov/publications/transitioning-use-cryptographic-algorithms-and-key-lengths + # + # Why did we use this public exponent? + # https://crypto.stanford.edu/~dabo/pubs/papers/RSA-survey.pdf + private_key = rsa.generate_private_key(public_exponent=65537, key_size=4096, backend=default_backend()) + + # Serialize the RSA private key to PEM encoding. + # This or DER encoding is likely to be what you get from your key management system in practice. + private_key_pem = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ) + + # Create the keyring that determines how your data keys are protected. + # + # If your key is encoded using DER, you can use RawRSAKeyring.from_der_encoding + keyring = RawRSAKeyring.from_pem_encoding( + # The key namespace and key name are defined by you + # and are used by the raw RSA keyring + # to determine whether it should attempt to decrypt + # an encrypted data key. + # + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-raw-rsa-keyring + key_namespace="some managed raw keys", + key_name=b"my RSA wrapping key", + private_encoded_key=private_key_pem, + # The wrapping algorithm tells the raw RSA keyring + # how to use your wrapping key to encrypt data keys. + # + # We recommend using RSA_OAEP_SHA256_MGF1. + # You should not use RSA_PKCS1 unless you require it for backwards compatibility. + wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, + ) + + # Encrypt your plaintext data. + ciphertext, _encrypt_header = aws_encryption_sdk.encrypt( + source=source_plaintext, encryption_context=encryption_context, keyring=keyring + ) + + # Demonstrate that the ciphertext and plaintext are different. + assert ciphertext != source_plaintext + + # Decrypt your encrypted data using the same keyring you used on encrypt. + # + # You do not need to specify the encryption context on decrypt + # because the header of the encrypted message includes the encryption context. + decrypted, decrypt_header = aws_encryption_sdk.decrypt(source=ciphertext, keyring=keyring) + + # Demonstrate that the decrypted plaintext is identical to the original plaintext. + assert decrypted == source_plaintext + + # Verify that the encryption context used in the decrypt operation includes + # the encryption context that you specified when encrypting. + # The AWS Encryption SDK can add pairs, so don't require an exact match. + # + # In production, always use a meaningful encryption context. + assert set(encryption_context.items()) <= set(decrypt_header.encryption_context.items()) diff --git a/examples/src/keyring/raw_rsa/public_private_key_separate.py b/examples/src/keyring/raw_rsa/public_private_key_separate.py new file mode 100644 index 000000000..b86b86ece --- /dev/null +++ b/examples/src/keyring/raw_rsa/public_private_key_separate.py @@ -0,0 +1,125 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +One of the benefits of asymmetric encryption +is that you can encrypt with just the public key. +This means that you can give someone the ability to encrypt +without giving them the ability to decrypt. + +The raw RSA keyring supports encrypt-only operations +when it only has access to a public key. + +This example shows how to construct and use the raw RSA keyring +to encrypt with only the public key and decrypt with the private key. + +If your RSA key is in PEM or DER format, +see the ``keyring/raw_rsa/private_key_only_from_pem`` example. + +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-raw-rsa-keyring + +In this example, we use the one-step encrypt and decrypt APIs. +""" +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.asymmetric import rsa + +import aws_encryption_sdk +from aws_encryption_sdk.exceptions import AWSEncryptionSDKClientError +from aws_encryption_sdk.identifiers import WrappingAlgorithm +from aws_encryption_sdk.keyrings.raw import RawRSAKeyring + + +def run(source_plaintext): + # type: (bytes) -> None + """Demonstrate an encrypt/decrypt cycle using separate public and private raw RSA keyrings. + + :param bytes source_plaintext: Plaintext to encrypt + """ + # Prepare your encryption context. + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + encryption_context = { + "encryption": "context", + "is not": "secret", + "but adds": "useful metadata", + "that can help you": "be confident that", + "the data you are handling": "is what you think it is", + } + + # Generate an RSA private key to use with your keyring. + # In practice, you should get this key from a secure key management system such as an HSM. + # + # The National Institute of Standards and Technology (NIST) recommends a minimum of 2048-bit keys for RSA. + # https://www.nist.gov/publications/transitioning-use-cryptographic-algorithms-and-key-lengths + # + # Why did we use this public exponent? + # https://crypto.stanford.edu/~dabo/pubs/papers/RSA-survey.pdf + private_key = rsa.generate_private_key(public_exponent=65537, key_size=4096, backend=default_backend()) + + # Collect the public key from the private key. + public_key = private_key.public_key() + + # The keyring determines how your data keys are protected. + # + # Create the encrypt keyring that only has access to the public key. + public_key_keyring = RawRSAKeyring( + # The key namespace and key name are defined by you + # and are used by the raw RSA keyring + # to determine whether it should attempt to decrypt + # an encrypted data key. + # + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-raw-rsa-keyring + key_namespace="some managed raw keys", + key_name=b"my RSA wrapping key", + public_wrapping_key=public_key, + # The wrapping algorithm tells the raw RSA keyring + # how to use your wrapping key to encrypt data keys. + # + # We recommend using RSA_OAEP_SHA256_MGF1. + # You should not use RSA_PKCS1 unless you require it for backwards compatibility. + wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, + ) + + # Create the decrypt keyring that has access to the private key. + private_key_keyring = RawRSAKeyring( + # The key namespace and key name MUST match the encrypt keyring. + key_namespace="some managed raw keys", + key_name=b"my RSA wrapping key", + private_wrapping_key=private_key, + # The wrapping algorithm MUST match the encrypt keyring. + wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, + ) + + # Encrypt your plaintext data using the encrypt keyring. + ciphertext, _encrypt_header = aws_encryption_sdk.encrypt( + source=source_plaintext, encryption_context=encryption_context, keyring=public_key_keyring + ) + + # Demonstrate that the ciphertext and plaintext are different. + assert ciphertext != source_plaintext + + # Try to decrypt your encrypted data using the *encrypt* keyring. + # This demonstrates that you cannot decrypt using the public key. + try: + aws_encryption_sdk.decrypt(source=ciphertext, keyring=public_key_keyring) + except AWSEncryptionSDKClientError: + # The public key cannot decrypt. + # Reaching this point means everything is working as expected. + pass + else: + # Show that the public keyring could not decrypt. + raise AssertionError("The public key can never decrypt!") + + # Decrypt your encrypted data using the decrypt keyring. + # + # You do not need to specify the encryption context on decrypt + # because the header of the encrypted message includes the encryption context. + decrypted, decrypt_header = aws_encryption_sdk.decrypt(source=ciphertext, keyring=private_key_keyring) + + # Demonstrate that the decrypted plaintext is identical to the original plaintext. + assert decrypted == source_plaintext + + # Verify that the encryption context used in the decrypt operation includes + # the encryption context that you specified when encrypting. + # The AWS Encryption SDK can add pairs, so don't require an exact match. + # + # In production, always use a meaningful encryption context. + assert set(encryption_context.items()) <= set(decrypt_header.encryption_context.items()) diff --git a/examples/src/legacy/multiple_kms_cmk_regions.py b/examples/src/legacy/multiple_kms_cmk_regions.py deleted file mode 100644 index deefd73e9..000000000 --- a/examples/src/legacy/multiple_kms_cmk_regions.py +++ /dev/null @@ -1,65 +0,0 @@ -# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: Apache-2.0 -""" -Example showing basic encryption and decryption of a value already in memory -using multiple KMS CMKs in multiple regions. -""" -import aws_encryption_sdk -from aws_encryption_sdk.key_providers.kms import KMSMasterKey, KMSMasterKeyProvider - - -def run(aws_kms_generator_cmk, aws_kms_additional_cmks, source_plaintext, botocore_session=None): - """Encrypts and then decrypts a string under multiple KMS customer master keys (CMKs) in multiple regions. - - :param str aws_kms_generator_cmk: Amazon Resource Name (ARN) of the primary KMS CMK - :param List[str] aws_kms_additional_cmks: Additional Amazon Resource Names (ARNs) of secondary KMS CMKs - :param bytes source_plaintext: Data to encrypt - :param botocore_session: existing botocore session instance - :type botocore_session: botocore.session.Session - """ - encrypt_cmk = aws_kms_additional_cmks[0] - - # Check that these keys are in different regions - assert not aws_kms_generator_cmk.split(":")[3] == encrypt_cmk.split(":")[3] - - kwargs = dict(key_ids=[aws_kms_generator_cmk, encrypt_cmk]) - - if botocore_session is not None: - kwargs["botocore_session"] = botocore_session - - # Create master key provider using the ARNs of the keys and the session (botocore_session) - kms_key_provider = KMSMasterKeyProvider(**kwargs) - - # Encrypt the plaintext using the AWS Encryption SDK. It returns the encrypted message and the header - ciphertext, encrypted_message_header = aws_encryption_sdk.encrypt( - key_provider=kms_key_provider, source=source_plaintext - ) - - # Check that both key ARNs are in the message headers - assert len(encrypted_message_header.encrypted_data_keys) == 2 - - # Decrypt the encrypted message using the AWS Encryption SDK. It returns the decrypted message and the header - # Either of our keys can be used to decrypt the message - plaintext_1, decrypted_message_header_1 = aws_encryption_sdk.decrypt( - key_provider=KMSMasterKey(key_id=aws_kms_generator_cmk), source=ciphertext - ) - plaintext_2, decrypted_message_header_2 = aws_encryption_sdk.decrypt( - key_provider=KMSMasterKey(key_id=encrypt_cmk), source=ciphertext - ) - - # Check that the original message and the decrypted message are the same - if not isinstance(source_plaintext, bytes): - plaintext_1 = plaintext_1.decode("utf-8") - plaintext_2 = plaintext_2.decode("utf-8") - assert source_plaintext == plaintext_1 - assert source_plaintext == plaintext_2 - - # Check that the headers of the encrypted message and decrypted message match - assert all( - pair in encrypted_message_header.encryption_context.items() - for pair in decrypted_message_header_1.encryption_context.items() - ) - assert all( - pair in encrypted_message_header.encryption_context.items() - for pair in decrypted_message_header_2.encryption_context.items() - ) diff --git a/examples/src/legacy/one_kms_cmk.py b/examples/src/legacy/one_kms_cmk.py deleted file mode 100644 index 7fb5f1431..000000000 --- a/examples/src/legacy/one_kms_cmk.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: Apache-2.0 -"""Example showing basic encryption and decryption of a value already in memory using one KMS CMK.""" -import aws_encryption_sdk - - -def run(aws_kms_cmk, source_plaintext, botocore_session=None): - """Encrypts and then decrypts a string under one KMS customer master key (CMK). - - :param str aws_kms_cmk: Amazon Resource Name (ARN) of the KMS CMK - :param bytes source_plaintext: Data to encrypt - :param botocore_session: existing botocore session instance - :type botocore_session: botocore.session.Session - """ - kwargs = dict(key_ids=[aws_kms_cmk]) - - if botocore_session is not None: - kwargs["botocore_session"] = botocore_session - - # Create master key provider using the ARN of the key and the session (botocore_session) - kms_key_provider = aws_encryption_sdk.KMSMasterKeyProvider(**kwargs) - - # Encrypt the plaintext using the AWS Encryption SDK. It returns the encrypted message and the header - ciphertext, encrypted_message_header = aws_encryption_sdk.encrypt( - source=source_plaintext, key_provider=kms_key_provider - ) - - # Decrypt the encrypted message using the AWS Encryption SDK. It returns the decrypted message and the header - plaintext, decrypted_message_header = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=kms_key_provider) - - # Check if the original message and the decrypted message are the same - assert source_plaintext == plaintext - - # Check if the headers of the encrypted message and decrypted message match - assert all( - pair in encrypted_message_header.encryption_context.items() - for pair in decrypted_message_header.encryption_context.items() - ) diff --git a/examples/src/onestep_defaults.py b/examples/src/onestep_defaults.py index a1d6e3dbd..3ded2b3ae 100644 --- a/examples/src/onestep_defaults.py +++ b/examples/src/onestep_defaults.py @@ -6,7 +6,7 @@ In this example, we use an AWS KMS customer master key (CMK), but you can use other key management options with the AWS Encryption SDK. For examples that demonstrate how to use other key management configurations, -see the ``keyring`` and ``mater_key_provider`` directories. +see the ``keyring`` and ``master_key_provider`` directories. """ import aws_encryption_sdk from aws_encryption_sdk.keyrings.aws_kms import KmsKeyring @@ -37,16 +37,16 @@ def run(aws_kms_cmk, source_plaintext): source=source_plaintext, encryption_context=encryption_context, keyring=keyring ) - # Verify that the ciphertext and plaintext are different. + # Demonstrate that the ciphertext and plaintext are different. assert ciphertext != source_plaintext # Decrypt your encrypted data using the same keyring you used on encrypt. # - # We do not need to specify the encryption context on decrypt - # because the header message includes the encryption context. + # You do not need to specify the encryption context on decrypt + # because the header of the encrypted message includes the encryption context. decrypted, decrypt_header = aws_encryption_sdk.decrypt(source=ciphertext, keyring=keyring) - # Verify that the decrypted plaintext is identical to the original plaintext. + # Demonstrate that the decrypted plaintext is identical to the original plaintext. assert decrypted == source_plaintext # Verify that the encryption context used in the decrypt operation includes diff --git a/examples/src/onestep_unsigned.py b/examples/src/onestep_unsigned.py index aafb09feb..72623087b 100644 --- a/examples/src/onestep_unsigned.py +++ b/examples/src/onestep_unsigned.py @@ -7,7 +7,7 @@ In this example, we use an AWS KMS customer master key (CMK), but you can use other key management options with the AWS Encryption SDK. For examples that demonstrate how to use other key management configurations, -see the ``keyring`` and ``mater_key_provider`` directories. +see the ``keyring`` and ``master_key_provider`` directories. The default algorithm suite includes a message-level signature that protects you from an attacker who has *decrypt* but not *encrypt* capability @@ -54,19 +54,19 @@ def run(aws_kms_cmk, source_plaintext): algorithm=AlgorithmSuite.AES_256_GCM_IV12_TAG16_HKDF_SHA256, ) - # Verify that the ciphertext and plaintext are different. + # Demonstrate that the ciphertext and plaintext are different. assert ciphertext != source_plaintext # Decrypt your encrypted data using the same keyring you used on encrypt. # - # We do not need to specify the encryption context on decrypt - # because the header message includes the encryption context. + # You do not need to specify the encryption context on decrypt + # because the header of the encrypted message includes the encryption context. # - # We do not need to specify the algorithm suite on decrypt + # You do not need to specify the algorithm suite on decrypt # because the header message includes the algorithm suite identifier. decrypted, decrypt_header = aws_encryption_sdk.decrypt(source=ciphertext, keyring=keyring) - # Verify that the decrypted plaintext is identical to the original plaintext. + # Demonstrate that the decrypted plaintext is identical to the original plaintext. assert decrypted == source_plaintext # Verify that the encryption context used in the decrypt operation includes diff --git a/examples/test/examples_test_utils.py b/examples/test/examples_test_utils.py index 4f9aadef5..49379ff1b 100644 --- a/examples/test/examples_test_utils.py +++ b/examples/test/examples_test_utils.py @@ -27,6 +27,7 @@ os.environ["AWS_ENCRYPTION_SDK_EXAMPLES_TESTING"] = "yes" sys.path.extend([os.sep.join([os.path.dirname(__file__), "..", "..", "test", "integration"])]) +from integration_test_utils import get_all_cmk_arns # noqa pylint: disable=unused-import,import-error static_plaintext = ( b"Lorem ipsum dolor sit amet, consectetur adipiscing elit. " @@ -58,9 +59,6 @@ ) -from integration_test_utils import get_all_cmk_arns # noqa pylint: disable=unused-import,import-error - - def all_examples(): # type: () -> Iterable[pytest.param] for (dirpath, _dirnames, filenames) in os.walk(EXAMPLES_SOURCE): @@ -70,7 +68,7 @@ def all_examples(): continue stem, suffix = split_path if suffix == "py" and stem != "__init__": - module_parent = dirpath[len(EXAMPLES_SOURCE) + 1 :].replace("/", ".") + module_parent = dirpath[len(EXAMPLES_SOURCE) + 1 :].replace(os.path.sep, ".") module_name = stem if module_parent: import_path = "..src.{base}.{name}".format(base=module_parent, name=module_name) @@ -114,3 +112,9 @@ def build_kwargs(function, temp_dir): except KeyError: pass return kwargs + + +def default_region(): + # type: () -> str + primary_cmk = get_all_cmk_arns()[0] + return primary_cmk.split(":", 4)[3] diff --git a/examples/test/test_run_examples.py b/examples/test/test_run_examples.py index 781f341ad..210c0119c 100644 --- a/examples/test/test_run_examples.py +++ b/examples/test/test_run_examples.py @@ -5,13 +5,13 @@ import pytest -from .examples_test_utils import all_examples, build_kwargs +from .examples_test_utils import all_examples, build_kwargs, default_region pytestmark = [pytest.mark.examples] @pytest.mark.parametrize("import_path", all_examples()) -def test_examples(import_path, tmp_path): +def test_examples(import_path, tmp_path, monkeypatch): module = import_module(name=import_path, package=__package__) try: run_function = module.run @@ -21,4 +21,6 @@ def test_examples(import_path, tmp_path): kwargs = build_kwargs(function=run_function, temp_dir=tmp_path) + monkeypatch.setenv("AWS_DEFAULT_REGION", default_region()) + run_function(**kwargs) diff --git a/src/aws_encryption_sdk/keyrings/raw.py b/src/aws_encryption_sdk/keyrings/raw.py index deab1a122..dfcc5294b 100644 --- a/src/aws_encryption_sdk/keyrings/raw.py +++ b/src/aws_encryption_sdk/keyrings/raw.py @@ -307,6 +307,7 @@ def from_der_encoding( private_encoded_key=None, # type: bytes password=None, # type: bytes ): + # type: (...) -> RawRSAKeyring """Generate a raw RSA keyring using DER Encoded public and private keys :param str key_namespace: String defining the keyring ID