|
| 1 | +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. |
| 2 | +# SPDX-License-Identifier: Apache-2.0 |
| 3 | +""" |
| 4 | +Encryption context is a powerful tool for access and audit controls |
| 5 | +because it lets you tie *non-secret* metadata about a plaintext value to the encrypted message. |
| 6 | +This is especially powerful when you use the AWS Encryption SDK with AWS KMS, |
| 7 | +but within the context of the AWS Encryption SDK alone |
| 8 | +you can use cryptographic materials managers to analyse the encryption context |
| 9 | +to provide logical controls and additional metadata. |
| 10 | +
|
| 11 | +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context |
| 12 | +https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#encrypt_context |
| 13 | +
|
| 14 | +This example shows how to create a custom cryptographic materials manager (CMM) |
| 15 | +that requires a particular field in any encryption context. |
| 16 | +""" |
| 17 | +import aws_encryption_sdk |
| 18 | +from aws_encryption_sdk.keyrings.aws_kms import KmsKeyring |
| 19 | +from aws_encryption_sdk.keyrings.base import Keyring |
| 20 | +from aws_encryption_sdk.materials_managers import ( |
| 21 | + DecryptionMaterials, |
| 22 | + DecryptionMaterialsRequest, |
| 23 | + EncryptionMaterials, |
| 24 | + EncryptionMaterialsRequest, |
| 25 | +) |
| 26 | +from aws_encryption_sdk.materials_managers.base import CryptoMaterialsManager |
| 27 | +from aws_encryption_sdk.materials_managers.default import DefaultCryptoMaterialsManager |
| 28 | + |
| 29 | + |
| 30 | +class MissingClassificationError(Exception): |
| 31 | + """Indicates that an encryption context was found that lacked a classification identifier.""" |
| 32 | + |
| 33 | + |
| 34 | +class ClassificationRequiringCryptoMaterialsManager(CryptoMaterialsManager): |
| 35 | + """Only allow requests when the encryption context contains a classification identifier.""" |
| 36 | + |
| 37 | + def __init__(self, keyring): |
| 38 | + # type: (Keyring) -> None |
| 39 | + """Set up the inner cryptographic materials manager using the provided keyring. |
| 40 | +
|
| 41 | + :param Keyring keyring: Keyring to use in the inner cryptographic materials manager |
| 42 | + """ |
| 43 | + self._classification_field = "classification" |
| 44 | + self._classification_error = MissingClassificationError("Encryption context does not contain classification!") |
| 45 | + self._cmm = DefaultCryptoMaterialsManager(keyring=keyring) |
| 46 | + |
| 47 | + def get_encryption_materials(self, request): |
| 48 | + # type: (EncryptionMaterialsRequest) -> EncryptionMaterials |
| 49 | + """Block any requests that do not contain a classification identifier in the encryption context.""" |
| 50 | + if self._classification_field not in request.encryption_context: |
| 51 | + raise self._classification_error |
| 52 | + |
| 53 | + return self._cmm.get_encryption_materials(request) |
| 54 | + |
| 55 | + def decrypt_materials(self, request): |
| 56 | + # type: (DecryptionMaterialsRequest) -> DecryptionMaterials |
| 57 | + """Block any requests that do not contain a classification identifier in the encryption context.""" |
| 58 | + if self._classification_field not in request.encryption_context: |
| 59 | + raise self._classification_error |
| 60 | + |
| 61 | + return self._cmm.decrypt_materials(request) |
| 62 | + |
| 63 | + |
| 64 | +def run(aws_kms_cmk, source_plaintext): |
| 65 | + # type: (str, bytes) -> None |
| 66 | + """Demonstrate an encrypt/decrypt cycle using a KMS keyring with a single CMK. |
| 67 | +
|
| 68 | + :param str aws_kms_cmk: The ARN of an AWS KMS CMK that protects data keys |
| 69 | + :param bytes source_plaintext: Plaintext to encrypt |
| 70 | + """ |
| 71 | + # Prepare your encryption context. |
| 72 | + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context |
| 73 | + encryption_context = { |
| 74 | + "encryption": "context", |
| 75 | + "is not": "secret", |
| 76 | + "but adds": "useful metadata", |
| 77 | + "that can help you": "be confident that", |
| 78 | + "the data you are handling": "is what you think it is", |
| 79 | + } |
| 80 | + |
| 81 | + # Create the keyring that determines how your data keys are protected. |
| 82 | + keyring = KmsKeyring(generator_key_id=aws_kms_cmk) |
| 83 | + |
| 84 | + # Create the filtering cryptographic materials manager using your keyring. |
| 85 | + cmm = ClassificationRequiringCryptoMaterialsManager(keyring=keyring) |
| 86 | + |
| 87 | + # Demonstrate that the filtering CMM will not let you encrypt without a classification identifier. |
| 88 | + try: |
| 89 | + aws_encryption_sdk.encrypt( |
| 90 | + source=source_plaintext, encryption_context=encryption_context, materials_manager=cmm, |
| 91 | + ) |
| 92 | + except MissingClassificationError: |
| 93 | + # Your encryption context did not contain a classification identifier. |
| 94 | + # Reaching this point means everything is working as expected. |
| 95 | + pass |
| 96 | + else: |
| 97 | + # The filtering CMM keeps this from happening. |
| 98 | + raise AssertionError("The filtering CMM does not let this happen!") |
| 99 | + |
| 100 | + # Encrypt your plaintext data. |
| 101 | + classified_ciphertext, _encrypt_header = aws_encryption_sdk.encrypt( |
| 102 | + source=source_plaintext, |
| 103 | + encryption_context=dict(classification="secret", **encryption_context), |
| 104 | + materials_manager=cmm, |
| 105 | + ) |
| 106 | + |
| 107 | + # Demonstrate that the ciphertext and plaintext are different. |
| 108 | + assert classified_ciphertext != source_plaintext |
| 109 | + |
| 110 | + # Decrypt your encrypted data using the same cryptographic materials manager you used on encrypt. |
| 111 | + # |
| 112 | + # You do not need to specify the encryption context on decrypt |
| 113 | + # because the header of the encrypted message includes the encryption context. |
| 114 | + decrypted, decrypt_header = aws_encryption_sdk.decrypt(source=classified_ciphertext, materials_manager=cmm) |
| 115 | + |
| 116 | + # Demonstrate that the decrypted plaintext is identical to the original plaintext. |
| 117 | + assert decrypted == source_plaintext |
| 118 | + |
| 119 | + # Verify that the encryption context used in the decrypt operation includes |
| 120 | + # the encryption context that you specified when encrypting. |
| 121 | + # The AWS Encryption SDK can add pairs, so don't require an exact match. |
| 122 | + # |
| 123 | + # In production, always use a meaningful encryption context. |
| 124 | + assert set(encryption_context.items()) <= set(decrypt_header.encryption_context.items()) |
| 125 | + |
| 126 | + # Now demonstrate the decrypt path of the filtering cryptographic materials manager. |
| 127 | + |
| 128 | + # Encrypt your plaintext using the keyring and do not include a classification identifier. |
| 129 | + unclassified_ciphertext, encrypt_header = aws_encryption_sdk.encrypt( |
| 130 | + source=source_plaintext, encryption_context=encryption_context, keyring=keyring |
| 131 | + ) |
| 132 | + |
| 133 | + assert "classification" not in encrypt_header.encryption_context |
| 134 | + |
| 135 | + # Demonstrate that the filtering CMM will not let you decrypt messages without classification identifiers. |
| 136 | + try: |
| 137 | + aws_encryption_sdk.decrypt(source=unclassified_ciphertext, materials_manager=cmm) |
| 138 | + except MissingClassificationError: |
| 139 | + # Your encryption context did not contain a classification identifier. |
| 140 | + # Reaching this point means everything is working as expected. |
| 141 | + pass |
| 142 | + else: |
| 143 | + # The filtering CMM keeps this from happening. |
| 144 | + raise AssertionError("The filtering CMM does not let this happen!") |
0 commit comments