diff --git a/.github/workflows/ci_decrypt-oracle.yaml b/.github/workflows/ci_decrypt-oracle.yaml new file mode 100644 index 000000000..d8ecff117 --- /dev/null +++ b/.github/workflows/ci_decrypt-oracle.yaml @@ -0,0 +1,53 @@ +name: Continuous Integration tests for the decrypt oracle + +on: + pull_request: + push: + # Run once a day + schedule: + - cron: '0 0 * * *' + +jobs: + tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v1 + with: + # The oracle runs in a Python 3.6 Lamba + python-version: 3.6 + - run: | + python -m pip install --upgrade pip + pip install --upgrade -r ci-requirements.txt + - name: run test + env: + TOXENV: local + run: | + cd decrypt_oracle + tox -- -vv + static-analysis: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + category: + - bandit + - readme + - flake8 + - pylint + - flake8-tests + - pylint-tests + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v1 + with: + python-version: 3.x + - run: | + python -m pip install --upgrade pip + pip install --upgrade -r ci-requirements.txt + - name: run test + env: + TOXENV: ${{ matrix.category }} + run: | + cd decrypt_oracle + tox -- -vv diff --git a/.github/workflows/ci_static-analysis.yaml b/.github/workflows/ci_static-analysis.yaml new file mode 100644 index 000000000..f80c429fe --- /dev/null +++ b/.github/workflows/ci_static-analysis.yaml @@ -0,0 +1,40 @@ +name: Static analysis checks + +on: + pull_request: + push: + # Run once a day + schedule: + - cron: '0 0 * * *' + +jobs: + analysis: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + category: + - bandit + - doc8 + - docs + - readme + - flake8 + - pylint + - flake8-tests + - pylint-tests + - flake8-examples + - pylint-examples + - black-check + - isort-check + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v1 + with: + python-version: 3.x + - run: | + python -m pip install --upgrade pip + pip install --upgrade -r ci-requirements.txt + - name: run test + env: + TOXENV: ${{ matrix.category }} + run: tox -- -vv diff --git a/.github/workflows/ci_test-vector-handler.yaml b/.github/workflows/ci_test-vector-handler.yaml new file mode 100644 index 000000000..570133231 --- /dev/null +++ b/.github/workflows/ci_test-vector-handler.yaml @@ -0,0 +1,88 @@ +name: Continuous Integration tests for the test vector handler + +on: + pull_request: + push: + # Run once a day + schedule: + - cron: '0 0 * * *' + +jobs: + tests: + # Leaving this defined but disabled + # until we address the credentials problem. + if: 1 == 0 + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + os: + - ubuntu-latest + - windows-latest + - macos-latest + python: + - 2.7 + - 3.5 + - 3.6 + - 3.7 + - 3.8 + - 3.x + architecture: + - x64 + - x86 + category: + - awses_1.3.3 + - awses_1.3.max + - awses_latest + exclude: + # x86 builds are only meaningful for Windows + - os: ubuntu-latest + architecture: x86 + - os: macos-latest + architecture: x86 + steps: + - uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.INTEG_AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.INTEG_AWS_SECRET_ACCESS_KEY }} + aws-region: us-west-2 + - uses: actions/checkout@v2 + - uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python }} + architecture: ${{ matrix.architecture }} + - run: | + python -m pip install --upgrade pip + pip install --upgrade -r ci-requirements.txt + - name: run test + env: + TOXENV: ${{ matrix.category }} + run: | + cd test_vector_handlers + tox -- -vv + static-analysis: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + category: + - bandit + - readme + - flake8 + - pylint + - flake8-tests + - pylint-tests + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v1 + with: + python-version: 3.x + - run: | + python -m pip install --upgrade pip + pip install --upgrade -r ci-requirements.txt + - name: run test + env: + TOXENV: ${{ matrix.category }} + run: | + cd test_vector_handlers + tox -- -vv diff --git a/.github/workflows/ci_tests.yaml b/.github/workflows/ci_tests.yaml new file mode 100644 index 000000000..c2f297ea2 --- /dev/null +++ b/.github/workflows/ci_tests.yaml @@ -0,0 +1,100 @@ +name: Continuous Integration tests + +on: + pull_request: + push: + # Run once a day + schedule: + - cron: '0 0 * * *' + +env: + AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID: | + arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f + AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID_2: | + arn:aws:kms:eu-central-1:658956600833:key/75414c93-5285-4b57-99c9-30c1cf0a22c2 + +jobs: + tests: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + os: + - ubuntu-latest + - windows-latest + - macos-latest + python: + - 2.7 + - 3.5 + - 3.6 + - 3.7 + - 3.8 + - 3.x + architecture: + - x64 + - x86 + category: + - local + - accept +# These require credentials. +# Enable them once we sort how to provide them. +# - integ +# - examples + exclude: + # x86 builds are only meaningful for Windows + - os: ubuntu-latest + architecture: x86 + - os: macos-latest + architecture: x86 + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python }} + architecture: ${{ matrix.architecture }} + - run: | + python -m pip install --upgrade pip + pip install --upgrade -r ci-requirements.txt + - name: run test + env: + TOXENV: ${{ matrix.category }} + run: tox -- -vv + upstream-py3: + runs-on: ubuntu-latest + strategy: + fail-fast: true + matrix: + category: + - nocmk + - test-upstream-requirements-py37 + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v1 + with: + python-version: 3.7 + - run: | + python -m pip install --upgrade pip + pip install --upgrade -r ci-requirements.txt + - name: run test + env: + TOXENV: ${{ matrix.category }} + run: tox -- -vv + upstream-py2: + runs-on: ubuntu-latest + strategy: + fail-fast: true + matrix: + category: + - test-upstream-requirements-py27 + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v1 + with: + python-version: 2.7 + - run: | + python -m pip install --upgrade pip + pip install --upgrade -r ci-requirements.txt + - name: run test + env: + TOXENV: ${{ matrix.category }} + run: tox -- -vv diff --git a/.travis.yml b/.travis.yml index 86702c536..a8ca00f68 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,132 +3,52 @@ language: python matrix: include: # CPython 2.7 - - python: 2.7 - env: TOXENV=py27-local - stage: Client Tests - python: 2.7 env: TOXENV=py27-integ stage: Client Tests - - python: 2.7 - env: TOXENV=py27-accept - stage: Client Tests - python: 2.7 env: TOXENV=py27-examples stage: Client Tests - # CPython 3.4 - - python: 3.4 - env: TOXENV=py34-local - stage: Client Tests - - python: 3.4 - env: TOXENV=py34-integ - stage: Client Tests - - python: 3.4 - env: TOXENV=py34-accept - stage: Client Tests - - python: 3.4 - env: TOXENV=py34-examples - stage: Client Tests # CPython 3.5 - - python: 3.5 - env: TOXENV=py35-local - stage: Client Tests - python: 3.5 env: TOXENV=py35-integ stage: Client Tests - - python: 3.5 - env: TOXENV=py35-accept - stage: Client Tests - python: 3.5 env: TOXENV=py35-examples stage: Client Tests # CPython 3.6 - - python: 3.6 - env: TOXENV=py36-local - stage: Client Tests - python: 3.6 env: TOXENV=py36-integ stage: Client Tests - - python: 3.6 - env: TOXENV=py36-accept - stage: Client Tests - python: 3.6 env: TOXENV=py36-examples stage: Client Tests # CPython 3.7 # xenial + sudo are currently needed to get 3.7 # https://github.com/travis-ci/travis-ci/issues/9815 - - python: 3.7 - env: TOXENV=py37-local - dist: xenial - sudo: true - stage: Client Tests - python: 3.7 env: TOXENV=py37-integ dist: xenial sudo: true stage: Client Tests - python: 3.7 - env: TOXENV=py37-accept + env: TOXENV=py37-examples dist: xenial sudo: true stage: Client Tests - - python: 3.7 - env: TOXENV=py37-examples + # CPython 3.8 + # xenial + sudo are currently needed to get 3.8 + # https://github.com/travis-ci/travis-ci/issues/9815 + - python: 3.8 + env: TOXENV=py38-integ dist: xenial sudo: true stage: Client Tests - # Upstream tests - - python: 3.6 - env: TOXENV=nocmk - stage: Upstream Tests - - python: 2.7 - env: TOXENV=test-upstream-requirements-py27 - stage: Upstream Tests - # xenial + sudo are currently needed to get 3.7 - # https://github.com/travis-ci/travis-ci/issues/9815 - - python: 3.7 - env: TOXENV=test-upstream-requirements-py37 + - python: 3.8 + env: TOXENV=py38-examples dist: xenial sudo: true - stage: Upstream Tests - # Security - - python: 3.6 - env: TOXENV=bandit - stage: Security Checks - # Linting and autoformatting - - python: 3.6 - env: TOXENV=doc8 - stage: Formatting Checks - - python: 3.6 - env: TOXENV=docs - stage: Formatting Checks - - python: 3.6 - env: TOXENV=readme - stage: Formatting Checks - - python: 3.6 - env: TOXENV=flake8 - stage: Formatting Checks - - python: 3.6 - env: TOXENV=pylint - stage: Formatting Checks - - python: 3.6 - env: TOXENV=flake8-tests - stage: Formatting Checks - - python: 3.6 - env: TOXENV=pylint-tests - stage: Formatting Checks - - python: 3.6 - env: TOXENV=flake8-examples - stage: Formatting Checks - - python: 3.6 - env: TOXENV=pylint-examples - stage: Formatting Checks - - python: 3.6 - env: TOXENV=black-check - stage: Formatting Checks - - python: 3.6 - env: TOXENV=isort-check - stage: Formatting Checks + stage: Client Tests ######################## # Test Vector Handlers # ######################## @@ -148,22 +68,6 @@ matrix: TEST_VECTOR_HANDLERS=1 TOXENV=py27-awses_latest stage: Test Vector Handler Tests - # CPython 3.4 - - python: 3.4 - env: - TEST_VECTOR_HANDLERS=1 - TOXENV=py34-awses_1.3.3 - stage: Test Vector Handler Tests - - python: 3.4 - env: - TEST_VECTOR_HANDLERS=1 - TOXENV=py34-awses_1.3.max - stage: Test Vector Handler Tests - - python: 3.4 - env: - TEST_VECTOR_HANDLERS=1 - TOXENV=py34-awses_latest - stage: Test Vector Handler Tests # CPython 3.5 - python: 3.5 env: @@ -218,93 +122,28 @@ matrix: dist: xenial sudo: true stage: Test Vector Handler Tests - # Linters - - python: 3.6 + # CPython 3.8 + - python: 3.8 env: TEST_VECTOR_HANDLERS=1 - TOXENV=bandit - stage: Test Vector Handler Formatting Checks - - python: 3.6 + TOXENV=py38-awses_1.3.3 + dist: xenial + sudo: true + stage: Test Vector Handler Tests + - python: 3.8 env: TEST_VECTOR_HANDLERS=1 - TOXENV=readme - stage: Test Vector Handler Formatting Checks - # Pending buildout of docs - #- python: 3.6 - # env: - # TEST_VECTOR_HANDLERS=1 - # TOXENV=docs - #- python: 3.6 - # env: - # TEST_VECTOR_HANDLERS=1 - # TOXENV=doc8 - # Pending linting cleanup - #- python: 3.6 - # env: - # TEST_VECTOR_HANDLERS=1 - # TOXENV=flake8 - #- python: 3.6 - # env: - # TEST_VECTOR_HANDLERS=1 - # TOXENV=pylint - #- python: 3.6 - # env: - # TEST_VECTOR_HANDLERS=1 - # TOXENV=flake8-tests - #- python: 3.6 - # env: - # TEST_VECTOR_HANDLERS=1 - # TOXENV=pylint-tests - ################## - # Decrypt Oracle # - ################## - # CPython 3.6 - # Because this build as Python 3.6 Lambda, this is the only runtime we are targetting. - - python: 3.6 - env: - DECRYPT_ORACLE=1 - TOXENV=py36-local - stage: Decrypt Oracle Tests - # Linters - - python: 3.6 - env: - DECRYPT_ORACLE=1 - TOXENV=bandit - stage: Decrypt Oracle Formatting Checks - - python: 3.6 - env: - DECRYPT_ORACLE=1 - TOXENV=readme - stage: Decrypt Oracle Formatting Checks - # Pending buildout of docs - #- python: 3.6 - # env: - # DECRYPT_ORACLE=1 - # TOXENV=docs - #- python: 3.6 - # env: - # DECRYPT_ORACLE=1 - # TOXENV=doc8 - - python: 3.6 - env: - DECRYPT_ORACLE=1 - TOXENV=flake8 - stage: Decrypt Oracle Formatting Checks - - python: 3.6 - env: - DECRYPT_ORACLE=1 - TOXENV=pylint - stage: Decrypt Oracle Formatting Checks - - python: 3.6 - env: - DECRYPT_ORACLE=1 - TOXENV=flake8-tests - stage: Decrypt Oracle Formatting Checks - - python: 3.6 + TOXENV=py38-awses_1.3.max + dist: xenial + sudo: true + stage: Test Vector Handler Tests + - python: 3.8 env: - DECRYPT_ORACLE=1 - TOXENV=pylint-tests - stage: Decrypt Oracle Formatting Checks + TEST_VECTOR_HANDLERS=1 + TOXENV=py38-awses_latest + dist: xenial + sudo: true + stage: Test Vector Handler Tests install: pip install tox script: - | diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d9bca1f73..aaaed8044 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,48 @@ Changelog ********* +1.5.0 -- 2020-xx-xx +=================== + +Major Features +-------------- + +* Add `keyrings`_. +* Change one-step APIs to return a :class:`CryptoResult` rather than a tuple. + + * Modified APIs: ``aws_encryption_sdk.encrypt`` and ``aws_encryption_sdk.decrypt``. + +.. note:: + + For backwards compatibility, + :class:`CryptoResult` also unpacks like a 2-member tuple. + This allows for backwards compatibility with the previous outputs + so this change should not break any existing consumers + unless you are specifically relying on the output being an instance of :class:`tuple`. + +Deprecations +------------ + +* Deprecate master key providers in favor of keyrings. + + * We still support using master key providers and are not removing them yet. + When we decide to remove them, + we will communicate that as defined in our versioning policy. + +* Deprecate support for Python 3.4. + + * This does not mean that this library will no longer work or install with 3.4, + but we are no longer testing against or advertising support for 3.4. + +Documentation +------------- + +* Added new examples demonstrating how to use + APIs, keyrings, cryptographic materials managers, and master key providers. + `#221 `_ + `#236 `_ + `#239 `_ + 1.4.1 -- 2019-09-20 =================== @@ -193,3 +235,4 @@ Minor .. _pylint: https://www.pylint.org/ .. _flake8: http://flake8.pycqa.org/en/latest/ .. _doc8: https://launchpad.net/doc8 +.. _keyrings: https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html diff --git a/README.rst b/README.rst index 7bc8038ea..3407dc177 100644 --- a/README.rst +++ b/README.rst @@ -38,7 +38,7 @@ Getting Started Required Prerequisites ====================== -* Python 2.7+ or 3.4+ +* Python 2.7 or 3.5+ * cryptography >= 1.8.1 * boto3 * attrs @@ -57,189 +57,42 @@ Installation Concepts ======== -There are four main concepts that you need to understand to use this library: +There are three main concepts that are helpful to understand when using the AWS Encryption SDK. + +For further information, see the `AWS Encryption SDK developer guide concepts`_. Cryptographic Materials Managers -------------------------------- -Cryptographic materials managers (CMMs) are resources that collect cryptographic materials and prepare them for -use by the Encryption SDK core logic. - -An example of a CMM is the default CMM, which is automatically generated anywhere a caller provides a master -key provider. The default CMM collects encrypted data keys from all master keys referenced by the master key -provider. - -An example of a more advanced CMM is the caching CMM, which caches cryptographic materials provided by another CMM. +The cryptographic materials manager (CMM) assembles the cryptographic materials +that are used to encrypt and decrypt data. -Master Key Providers --------------------- -Master key providers are resources that provide master keys. -An example of a master key provider is `AWS KMS`_. +`For more details, +see the AWS Encryption SDK developer guide cryptographic materials manager concept. +`_ -To encrypt data in this client, a ``MasterKeyProvider`` object must contain at least one ``MasterKey`` object. +Keyrings +-------- -``MasterKeyProvider`` objects can also contain other ``MasterKeyProvider`` objects. +A keyring generates, encrypts, and decrypts data keys. -Master Keys ------------ -Master keys generate, encrypt, and decrypt data keys. -An example of a master key is a `KMS customer master key (CMK)`_. +`For more details, +see the AWS Encryption SDK developer guide keyring concept. +`_ Data Keys --------- -Data keys are the encryption keys that are used to encrypt your data. If your algorithm suite -uses a key derivation function, the data key is used to generate the key that directly encrypts the data. + +A data key is an encryption key that the AWS Encryption SDK uses to encrypt your data. + +`For more details, +see the AWS Encryption SDK developer guide data key concept. +`_ ***** Usage ***** -To use this client, you (the caller) must provide an instance of either a master key provider -or a CMM. The examples in this readme use the ``KMSMasterKeyProvider`` class. - -KMSMasterKeyProvider -==================== -Because the ``KMSMasterKeyProvider`` uses the `boto3 SDK`_ to interact with `AWS KMS`_, it requires AWS Credentials. -To provide these credentials, use the `standard means by which boto3 locates credentials`_ or provide a -pre-existing instance of a ``botocore session`` to the ``KMSMasterKeyProvider``. -This latter option can be useful if you have an alternate way to store your AWS credentials or -you want to reuse an existing instance of a botocore session in order to decrease startup costs. - -.. code:: python - - import aws_encryption_sdk - import botocore.session - - kms_key_provider = aws_encryption_sdk.KMSMasterKeyProvider() - - existing_botocore_session = botocore.session.Session() - kms_key_provider = aws_encryption_sdk.KMSMasterKeyProvider(botocore_session=existing_botocore_session) - - -You can pre-load the ``KMSMasterKeyProvider`` with one or more CMKs. -To encrypt data, you must configure the ``KMSMasterKeyProvider`` with as least one CMK. -If you configure the the ``KMSMasterKeyProvider`` with multiple CMKs, the `final message`_ -will include a copy of the data key encrypted by each configured CMK. - -.. code:: python - - import aws_encryption_sdk - - kms_key_provider = aws_encryption_sdk.KMSMasterKeyProvider(key_ids=[ - 'arn:aws:kms:us-east-1:2222222222222:key/22222222-2222-2222-2222-222222222222', - 'arn:aws:kms:us-east-1:3333333333333:key/33333333-3333-3333-3333-333333333333' - ]) - -You can add CMKs from multiple regions to the ``KMSMasterKeyProvider``. - -.. code:: python - - import aws_encryption_sdk - - kms_key_provider = aws_encryption_sdk.KMSMasterKeyProvider(key_ids=[ - 'arn:aws:kms:us-east-1:2222222222222:key/22222222-2222-2222-2222-222222222222', - 'arn:aws:kms:us-west-2:3333333333333:key/33333333-3333-3333-3333-333333333333', - 'arn:aws:kms:ap-northeast-1:4444444444444:key/44444444-4444-4444-4444-444444444444' - ]) - - -Encryption and Decryption -========================= -After you create an instance of a ``MasterKeyProvider``, you can use either of the two -high-level ``encrypt``/``decrypt`` functions to encrypt and decrypt your data. - -.. code:: python - - import aws_encryption_sdk - - kms_key_provider = aws_encryption_sdk.KMSMasterKeyProvider(key_ids=[ - 'arn:aws:kms:us-east-1:2222222222222:key/22222222-2222-2222-2222-222222222222', - 'arn:aws:kms:us-east-1:3333333333333:key/33333333-3333-3333-3333-333333333333' - ]) - my_plaintext = b'This is some super secret data! Yup, sure is!' - - my_ciphertext, encryptor_header = aws_encryption_sdk.encrypt( - source=my_plaintext, - key_provider=kms_key_provider - ) - - decrypted_plaintext, decryptor_header = aws_encryption_sdk.decrypt( - source=my_ciphertext, - key_provider=kms_key_provider - ) - - assert my_plaintext == decrypted_plaintext - assert encryptor_header.encryption_context == decryptor_header.encryption_context - -You can provide an `encryption context`_: a form of additional authenticating information. - -.. code:: python - - import aws_encryption_sdk - - kms_key_provider = aws_encryption_sdk.KMSMasterKeyProvider(key_ids=[ - 'arn:aws:kms:us-east-1:2222222222222:key/22222222-2222-2222-2222-222222222222', - 'arn:aws:kms:us-east-1:3333333333333:key/33333333-3333-3333-3333-333333333333' - ]) - my_plaintext = b'This is some super secret data! Yup, sure is!' - - my_ciphertext, encryptor_header = aws_encryption_sdk.encrypt( - source=my_plaintext, - key_provider=kms_key_provider, - encryption_context={ - 'not really': 'a secret', - 'but adds': 'some authentication' - } - ) - - decrypted_plaintext, decryptor_header = aws_encryption_sdk.decrypt( - source=my_ciphertext, - key_provider=kms_key_provider - ) - - assert my_plaintext == decrypted_plaintext - assert encryptor_header.encryption_context == decryptor_header.encryption_context - - -Streaming -========= -If you are handling large files or simply do not want to put the entire plaintext or ciphertext in -memory at once, you can use this library's streaming clients directly. The streaming clients are -file-like objects, and behave exactly as you would expect a Python file object to behave, -offering context manager and iteration support. - -.. code:: python - - import aws_encryption_sdk - import filecmp - - kms_key_provider = aws_encryption_sdk.KMSMasterKeyProvider(key_ids=[ - 'arn:aws:kms:us-east-1:2222222222222:key/22222222-2222-2222-2222-222222222222', - 'arn:aws:kms:us-east-1:3333333333333:key/33333333-3333-3333-3333-333333333333' - ]) - plaintext_filename = 'my-secret-data.dat' - ciphertext_filename = 'my-encrypted-data.ct' - - with open(plaintext_filename, 'rb') as pt_file, open(ciphertext_filename, 'wb') as ct_file: - with aws_encryption_sdk.stream( - mode='e', - source=pt_file, - key_provider=kms_key_provider - ) as encryptor: - for chunk in encryptor: - ct_file.write(chunk) - - new_plaintext_filename = 'my-decrypted-data.dat' - with open(ciphertext_filename, 'rb') as ct_file, open(new_plaintext_filename, 'wb') as pt_file: - with aws_encryption_sdk.stream( - mode='d', - source=ct_file, - key_provider=kms_key_provider - ) as decryptor: - for chunk in decryptor: - pt_file.write(chunk) - - assert filecmp.cmp(plaintext_filename, new_plaintext_filename) - assert encryptor.header.encryption_context == decryptor.header.encryption_context +For examples of how to use these concepts to accomplish different tasks, see our `examples`_. Performance Considerations ========================== @@ -251,6 +104,8 @@ to your use-case in order to obtain peak performance. .. _AWS Encryption SDK: https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/introduction.html +.. _AWS Encryption SDK developer guide concepts: + https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html .. _cryptography: https://cryptography.io/en/latest/ .. _cryptography installation guide: https://cryptography.io/en/latest/installation/ .. _Read the Docs: http://aws-encryption-sdk-python.readthedocs.io/en/latest/ @@ -261,4 +116,5 @@ to your use-case in order to obtain peak performance. .. _standard means by which boto3 locates credentials: https://boto3.readthedocs.io/en/latest/guide/configuration.html .. _final message: https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/message-format.html .. _encryption context: https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#encrypt_context -.. _Security issue notifications: ./CONTRIBUTING.md#security-issue-notifications +.. _examples: https://github.com/aws/aws-encryption-sdk-python/tree/master/examples +.. _Security issue notifications: https://github.com/aws/aws-encryption-sdk-python/tree/master/CONTRIBUTING.md#security-issue-notifications diff --git a/appveyor.yml b/appveyor.yml index cfb4bdcdb..d7791f6ae 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -7,99 +7,55 @@ environment: # analysis, etc are only run on Linux (via Travis CI). # Python 2.7 - - PYTHON: "C:\\Python27" - TOXENV: "py27-local" - PYTHON: "C:\\Python27" TOXENV: "py27-integ" - - PYTHON: "C:\\Python27" - TOXENV: "py27-accept" - PYTHON: "C:\\Python27" TOXENV: "py27-examples" - - PYTHON: "C:\\Python27-x64" - TOXENV: "py27-local" - PYTHON: "C:\\Python27-x64" TOXENV: "py27-integ" - - PYTHON: "C:\\Python27-x64" - TOXENV: "py27-accept" - PYTHON: "C:\\Python27-x64" TOXENV: "py27-examples" - # Python 3.4 - - PYTHON: "C:\\Python34" - TOXENV: "py34-local" - - PYTHON: "C:\\Python34" - TOXENV: "py34-integ" - - PYTHON: "C:\\Python34" - TOXENV: "py34-accept" - - PYTHON: "C:\\Python34" - TOXENV: "py34-examples" - - PYTHON: "C:\\Python34-x64" - DISTUTILS_USE_SDK: "1" - TOXENV: "py34-local" - - PYTHON: "C:\\Python34-x64" - DISTUTILS_USE_SDK: "1" - TOXENV: "py34-integ" - - PYTHON: "C:\\Python34-x64" - DISTUTILS_USE_SDK: "1" - TOXENV: "py34-accept" - - PYTHON: "C:\\Python34-x64" - DISTUTILS_USE_SDK: "1" - TOXENV: "py34-examples" - # Python 3.5 - - PYTHON: "C:\\Python35" - TOXENV: "py35-local" - PYTHON: "C:\\Python35" TOXENV: "py35-integ" - - PYTHON: "C:\\Python35" - TOXENV: "py35-accept" - PYTHON: "C:\\Python35" TOXENV: "py35-examples" - - PYTHON: "C:\\Python35-x64" - TOXENV: "py35-local" - PYTHON: "C:\\Python35-x64" TOXENV: "py35-integ" - - PYTHON: "C:\\Python35-x64" - TOXENV: "py35-accept" - PYTHON: "C:\\Python35-x64" TOXENV: "py35-examples" # Python 3.6 - - PYTHON: "C:\\Python36" - TOXENV: "py36-local" - PYTHON: "C:\\Python36" TOXENV: "py36-integ" - - PYTHON: "C:\\Python36" - TOXENV: "py36-accept" - PYTHON: "C:\\Python36" TOXENV: "py36-examples" - - PYTHON: "C:\\Python36-x64" - TOXENV: "py36-local" - PYTHON: "C:\\Python36-x64" TOXENV: "py36-integ" - - PYTHON: "C:\\Python36-x64" - TOXENV: "py36-accept" - PYTHON: "C:\\Python36-x64" TOXENV: "py36-examples" # Python 3.7 - - PYTHON: "C:\\Python37" - TOXENV: "py37-local" - PYTHON: "C:\\Python37" TOXENV: "py37-integ" - - PYTHON: "C:\\Python37" - TOXENV: "py37-accept" - PYTHON: "C:\\Python37" TOXENV: "py37-examples" - - PYTHON: "C:\\Python37-x64" - TOXENV: "py37-local" - PYTHON: "C:\\Python37-x64" TOXENV: "py37-integ" - - PYTHON: "C:\\Python37-x64" - TOXENV: "py37-accept" - PYTHON: "C:\\Python37-x64" TOXENV: "py37-examples" + # Python 3.8 + - PYTHON: "C:\\Python38" + TOXENV: "py38-integ" + - PYTHON: "C:\\Python38" + TOXENV: "py38-examples" + - PYTHON: "C:\\Python38-x64" + TOXENV: "py38-integ" + - PYTHON: "C:\\Python38-x64" + TOXENV: "py38-examples" + install: # Prepend newly installed Python to the PATH of this build - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" diff --git a/ci-requirements.txt b/ci-requirements.txt new file mode 100644 index 000000000..053148f84 --- /dev/null +++ b/ci-requirements.txt @@ -0,0 +1 @@ +tox diff --git a/doc/conf.py b/doc/conf.py index 2164a52a6..42a7771b4 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -67,7 +67,10 @@ def get_version(): htmlhelp_basename = "%sdoc" % project # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {"http://docs.python.org/": None} +intersphinx_mapping = { + "python": ("http://docs.python.org/3/", None), + "cryptography": ("https://cryptography.io/en/latest/", None), +} # autosummary autosummary_generate = True diff --git a/doc/index.rst b/doc/index.rst index 10957074e..2e21bccbd 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -14,6 +14,11 @@ Modules aws_encryption_sdk.caches.base aws_encryption_sdk.caches.local aws_encryption_sdk.caches.null + aws_encryption_sdk.keyrings.base + aws_encryption_sdk.keyrings.aws_kms + aws_encryption_sdk.keyrings.aws_kms.client_suppliers + aws_encryption_sdk.keyrings.multi + aws_encryption_sdk.keyrings.raw aws_encryption_sdk.key_providers.base aws_encryption_sdk.key_providers.kms aws_encryption_sdk.key_providers.raw @@ -37,6 +42,8 @@ Modules aws_encryption_sdk.internal.formatting.serialize aws_encryption_sdk.internal.str_ops aws_encryption_sdk.internal.structures + aws_encryption_sdk.internal.validators aws_encryption_sdk.internal.utils + aws_encryption_sdk.keyrings.aws_kms._client_cache .. include:: ../CHANGELOG.rst diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 000000000..081e62fab --- /dev/null +++ b/examples/README.md @@ -0,0 +1,138 @@ +# AWS Encryption SDK Examples + +This section features examples that show you +how to use the AWS Encryption SDK. +We demonstrate how to use the encryption and decryption APIs +and how to set up some common configuration patterns. + +## APIs + +The AWS Encryption SDK provides two high-level APIs: +one-step APIs that process the entire operation in memory +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 encryption and decryption APIs, +you need to describe how you want the library to protect your data keys. +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) + * [with master key providers](./src/master_key_provider/aws_kms/single_cmk.py) + * How to use multiple AWS KMS CMKs in different regions + * [with keyrings](./src/keyring/aws_kms/multiple_regions.py) + * [with master key providers](./src/master_key_provider/aws_kms/multiple_regions.py) + * How to decrypt when you don't know the CMK + * [with keyrings](./src/keyring/aws_kms/discovery_decrypt.py) + * [with master key providers](./src/master_key_provider/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) + * [with master key providers](./src/master_key_provider/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) + * [with master key providers](./src/master_key_provider/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) + * [with master key providers](./src/master_key_provider/multi/aws_kms_with_escrow.py) +* How to reuse data keys across multiple messages + * [with the caching cryptographic materials manager](./src/crypto_materials_manager/caching/simple_cache.py) +* How to restrict algorithm suites + * [with a custom cryptographic materials manager](src/crypto_materials_manager/custom/algorithm_suite_enforcement.py) +* How to require encryption context fields + * [with a custom cryptographic materials manager](src/crypto_materials_manager/custom/requiring_encryption_context_fields.py) + +### Keyrings + +Keyrings are the most common way for you to configure the AWS Encryption SDK. +They determine how the AWS Encryption SDK protects your data. +You can find these examples in [`examples/src/keyring`](./src/keyring). + +### Cryptographic Materials Managers + +Keyrings define how your data keys are protected, +but there is more going on here than just protecting data keys. + +Cryptographic materials managers give you higher-level controls +over how the AWS Encryption SDK protects your data. +This can include things like +enforcing the use of certain algorithm suites or encryption context settings, +reusing data keys across messages, +or changing how you interact with keyrings. +You can find these examples in +[`examples/src/crypto_materials_manager`](./src/crypto_materials_manager). + +### Master Key Providers + +Before there were keyrings, there were master key providers. +Master key providers were the original configuration structure +that we provided for defining how you want to protect your data keys. +Keyrings provide a simpler experience and often more powerful configuration options, +but if you need to use master key providers, +need help migrating from master key providers to keyrings, +or simply want to see the difference between these configuration experiences, +you can find these examples in [`examples/src/master_key_provider`](./src/master_key_provider). + +## Legacy + +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). + +# Writing Examples + +If you want to contribute a new example, that's awesome! +To make sure that your example is tested in our CI, +please make sure that it meets the following requirements: + +1. The example MUST be a distinct module in the [`examples/src/`](./src) directory. +1. The example MAY be nested arbitrarily deeply, + but every intermediate directory MUST contain a `__init__.py` file + so that CPython 2.7 will recognize it as a module. +1. Every example MUST be CPython 2.7 compatible. +1. Each example file MUST contain exactly one example. +1. Each example file MUST contain a function called `run` that runs the example. +1. If your `run` function needs any of the following inputs, + the parameters MUST have the following names: + * `aws_kms_cmk` (`str`) : A single AWS KMS CMK ARN. + * NOTE: You can assume that automatically discovered credentials have + `kms:GenerateDataKey`, `kms:Encrypt`, and `kms:Decrypt` permissions on this CMK. + * `aws_kms_generator_cmk` (`str`) : A single AWS KMS CMK ARN to use as a generator key. + * NOTE: You can assume that automatically discovered credentials have + `kms:GenerateDataKey`, `kms:Encrypt`, and `kms:Decrypt` permissions on this CMK. + * `aws_kms_additional_cmks` (`List[str]`) : + A list of AWS KMS CMK ARNs to use for encrypting and decrypting data keys. + * NOTE: You can assume that automatically discovered credentials have + `kms:Encrypt` and `kms:Decrypt` permissions on these CMKs. + * `source_plaintext` (`bytes`) : Plaintext data to encrypt. + * `source_plaintext_filename` (`str`) : A path to a file containing plaintext to encrypt. + * NOTE: You can assume that you have write access to the parent directory + and that anything you do in that directory will be cleaned up + by our test runners. +1. Any additional parameters MUST be optional. diff --git a/examples/src/crypto_materials_manager/__init__.py b/examples/src/crypto_materials_manager/__init__.py new file mode 100644 index 000000000..f413e63bd --- /dev/null +++ b/examples/src/crypto_materials_manager/__init__.py @@ -0,0 +1,7 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Cryptographic materials manager examples. + +These examples show how to create and use cryptographic materials managers. +""" diff --git a/examples/src/crypto_materials_manager/caching/__init__.py b/examples/src/crypto_materials_manager/caching/__init__.py new file mode 100644 index 000000000..2c55faad8 --- /dev/null +++ b/examples/src/crypto_materials_manager/caching/__init__.py @@ -0,0 +1,7 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Caching cryptographic materials manager examples. + +These examples show how to configure and use the caching cryptographic materials manager. +""" diff --git a/examples/src/crypto_materials_manager/caching/simple_cache.py b/examples/src/crypto_materials_manager/caching/simple_cache.py new file mode 100644 index 000000000..bb14b25fe --- /dev/null +++ b/examples/src/crypto_materials_manager/caching/simple_cache.py @@ -0,0 +1,94 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +The default cryptographic materials manager (CMM) +creates new encryption and decryption materials +on every call. +This means every encrypted message is protected by a unique data key, +but it also means that you need to interact with your key management system +in order to process any message. +If this causes performance, operations, or cost issues for you, +you might benefit from data key caching. + +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/data-key-caching.html + +This example shows how to configure the caching CMM +to reuse data keys across multiple encrypted messages. + +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 ``master_key_provider`` directories. + +In this example, we use the one-step encrypt and decrypt APIs. +""" +import aws_encryption_sdk +from aws_encryption_sdk.caches.local import LocalCryptoMaterialsCache +from aws_encryption_sdk.keyrings.aws_kms import KmsKeyring +from aws_encryption_sdk.materials_managers.caching import CachingCryptoMaterialsManager + + +def run(aws_kms_cmk, source_plaintext): + # type: (str, bytes) -> None + """Demonstrate an encrypt/decrypt cycle using the caching cryptographic materials manager. + + :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) + + # Create the caching cryptographic materials manager using your keyring. + cmm = CachingCryptoMaterialsManager( + keyring=keyring, + # The cache is where the caching CMM stores the materials. + # + # LocalCryptoMaterialsCache gives you a local, in-memory, cache. + cache=LocalCryptoMaterialsCache(capacity=100), + # max_age determines how long the caching CMM will reuse materials. + # + # This example uses two minutes. + # In production, always choose as small a value as possible + # that works for your requirements. + max_age=120.0, + # max_messages_encrypted determines how many messages + # the caching CMM will protect with the same materials. + # + # In production, always choose as small a value as possible + # that works for your requirements. + max_messages_encrypted=10, + ) + + # Encrypt your plaintext data. + ciphertext, _encrypt_header = aws_encryption_sdk.encrypt( + source=source_plaintext, encryption_context=encryption_context, materials_manager=cmm + ) + + # Demonstrate that the ciphertext and plaintext are different. + assert ciphertext != source_plaintext + + # Decrypt your encrypted data using the same cryptographic materials manager 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, materials_manager=cmm) + + # 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/crypto_materials_manager/custom/__init__.py b/examples/src/crypto_materials_manager/custom/__init__.py new file mode 100644 index 000000000..202647480 --- /dev/null +++ b/examples/src/crypto_materials_manager/custom/__init__.py @@ -0,0 +1,10 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Custom cryptographic materials manager (CMM) examples. + +The AWS Encryption SDK includes CMMs for common use cases, +but you might need to do something else. + +These examples show how you could create your own CMM for some specific requirements. +""" diff --git a/examples/src/crypto_materials_manager/custom/algorithm_suite_enforcement.py b/examples/src/crypto_materials_manager/custom/algorithm_suite_enforcement.py new file mode 100644 index 000000000..d7385fc76 --- /dev/null +++ b/examples/src/crypto_materials_manager/custom/algorithm_suite_enforcement.py @@ -0,0 +1,138 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +The AWS Encryption SDK supports several different algorithm suites +that offer different security properties. + +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/supported-algorithms.html + +By default, the AWS Encryption SDK will let you use any of these, +but you might want to restrict that further. + +We recommend that you use the default algorithm suite, +which uses AES-GCM with 256-bit keys, HKDF, and ECDSA message signing. +If your readers and writers have the same permissions, +you might want to omit the message signature for faster operation. +For more information about choosing a signed or unsigned algorithm suite, +see the AWS Encryption SDK developer guide: + +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/supported-algorithms.html#other-algorithms + +This example shows how you can make a custom cryptographic materials manager (CMM) +that only allows encrypt requests that either specify one of these two algorithm suites +or do not specify an algorithm suite, in which case the default CMM uses the default algorithm suite. +""" +import aws_encryption_sdk +from aws_encryption_sdk.identifiers import AlgorithmSuite +from aws_encryption_sdk.keyrings.aws_kms import KmsKeyring +from aws_encryption_sdk.keyrings.base import Keyring +from aws_encryption_sdk.materials_managers import ( + DecryptionMaterials, + DecryptionMaterialsRequest, + EncryptionMaterials, + EncryptionMaterialsRequest, +) +from aws_encryption_sdk.materials_managers.base import CryptoMaterialsManager +from aws_encryption_sdk.materials_managers.default import DefaultCryptoMaterialsManager + + +class UnapprovedAlgorithmSuite(Exception): + """Indicate that an unsupported algorithm suite was requested.""" + + +class RequireApprovedAlgorithmSuitesCryptoMaterialsManager(CryptoMaterialsManager): + """Only allow encryption requests for approved algorithm suites.""" + + def __init__(self, keyring): + # type: (Keyring) -> None + """Set up the inner cryptographic materials manager using the provided keyring. + + :param Keyring keyring: Keyring to use in the inner cryptographic materials manager + """ + self._allowed_algorithm_suites = { + None, # no algorithm suite in the request + AlgorithmSuite.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, # the default algorithm suite + AlgorithmSuite.AES_256_GCM_IV12_TAG16_HKDF_SHA256, # the recommended unsigned algorithm suite + } + # Wrap the provided keyring in the default cryptographic materials manager (CMM). + # + # This is the same thing that the encrypt and decrypt APIs, as well as the caching CMM, + # do if you provide a keyring instead of a CMM. + self._cmm = DefaultCryptoMaterialsManager(keyring=keyring) + + def get_encryption_materials(self, request): + # type: (EncryptionMaterialsRequest) -> EncryptionMaterials + """Block any requests that include an unapproved algorithm suite.""" + if request.algorithm not in self._allowed_algorithm_suites: + raise UnapprovedAlgorithmSuite("Unapproved algorithm suite requested!") + + return self._cmm.get_encryption_materials(request) + + def decrypt_materials(self, request): + # type: (DecryptionMaterialsRequest) -> DecryptionMaterials + """Be more permissive on decrypt and just pass through.""" + return self._cmm.decrypt_materials(request) + + +def run(aws_kms_cmk, source_plaintext): + # type: (str, bytes) -> None + """Demonstrate an encrypt/decrypt cycle using a custom cryptographic materials manager that filters requests. + + :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) + + # Create the algorithm suite restricting cryptographic materials manager using your keyring. + cmm = RequireApprovedAlgorithmSuitesCryptoMaterialsManager(keyring=keyring) + + # Demonstrate that the algorithm suite restricting CMM will not let you use an unapproved algorithm suite. + try: + aws_encryption_sdk.encrypt( + source=source_plaintext, + encryption_context=encryption_context, + materials_manager=cmm, + algorithm=AlgorithmSuite.AES_256_GCM_IV12_TAG16, + ) + except UnapprovedAlgorithmSuite: + # You asked for an unapproved algorithm suite. + # Reaching this point means everything is working as expected. + pass + else: + # The algorithm suite restricting CMM keeps this from happening. + raise AssertionError("The algorithm suite restricting CMM does not let this happen!") + + # Encrypt your plaintext data. + ciphertext, _encrypt_header = aws_encryption_sdk.encrypt( + source=source_plaintext, encryption_context=encryption_context, materials_manager=cmm + ) + + # Demonstrate that the ciphertext and plaintext are different. + assert ciphertext != source_plaintext + + # Decrypt your encrypted data using the same cryptographic materials manager 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, materials_manager=cmm) + + # 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/crypto_materials_manager/custom/requiring_encryption_context_fields.py b/examples/src/crypto_materials_manager/custom/requiring_encryption_context_fields.py new file mode 100644 index 000000000..c797849bb --- /dev/null +++ b/examples/src/crypto_materials_manager/custom/requiring_encryption_context_fields.py @@ -0,0 +1,153 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Encryption context is a powerful tool for access and audit controls +because it lets you tie *non-secret* metadata about a plaintext value to the encrypted message. +Within the AWS Encryption SDK, +you can use cryptographic materials managers to analyse the encryption context +to provide logical controls and additional metadata. + +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + +If you are using the AWS Encryption SDK with AWS KMS, +you can use AWS KMS to provide additional powerful controls using the encryption context. +For more information on that, see the KMS developer guide: + +https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#encrypt_context + +This example shows how to create a custom cryptographic materials manager (CMM) +that requires a particular field in the encryption context. +""" +import aws_encryption_sdk +from aws_encryption_sdk.keyrings.aws_kms import KmsKeyring +from aws_encryption_sdk.keyrings.base import Keyring +from aws_encryption_sdk.materials_managers import ( + DecryptionMaterials, + DecryptionMaterialsRequest, + EncryptionMaterials, + EncryptionMaterialsRequest, +) +from aws_encryption_sdk.materials_managers.base import CryptoMaterialsManager +from aws_encryption_sdk.materials_managers.default import DefaultCryptoMaterialsManager + + +class MissingClassificationError(Exception): + """Indicates that an encryption context was found that lacked a classification identifier.""" + + +class ClassificationRequiringCryptoMaterialsManager(CryptoMaterialsManager): + """Only allow requests when the encryption context contains a classification identifier.""" + + def __init__(self, keyring): + # type: (Keyring) -> None + """Set up the inner cryptographic materials manager using the provided keyring. + + :param Keyring keyring: Keyring to use in the inner cryptographic materials manager + """ + self._classification_field = "classification" + self._classification_error = MissingClassificationError("Encryption context does not contain classification!") + # Wrap the provided keyring in the default cryptographic materials manager (CMM). + # + # This is the same thing that the encrypt and decrypt APIs, as well as the caching CMM, + # do if you provide a keyring instead of a CMM. + self._cmm = DefaultCryptoMaterialsManager(keyring=keyring) + + def get_encryption_materials(self, request): + # type: (EncryptionMaterialsRequest) -> EncryptionMaterials + """Block any requests that do not contain a classification identifier in the encryption context.""" + if self._classification_field not in request.encryption_context: + raise self._classification_error + + return self._cmm.get_encryption_materials(request) + + def decrypt_materials(self, request): + # type: (DecryptionMaterialsRequest) -> DecryptionMaterials + """Block any requests that do not contain a classification identifier in the encryption context.""" + if self._classification_field not in request.encryption_context: + raise self._classification_error + + return self._cmm.decrypt_materials(request) + + +def run(aws_kms_cmk, source_plaintext): + # type: (str, bytes) -> None + """Demonstrate an encrypt/decrypt cycle using a custom cryptographic materials manager that filters requests. + + :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) + + # Create the classification requiring cryptographic materials manager using your keyring. + cmm = ClassificationRequiringCryptoMaterialsManager(keyring=keyring) + + # Demonstrate that the classification requiring CMM will not let you encrypt without a classification identifier. + try: + aws_encryption_sdk.encrypt( + source=source_plaintext, encryption_context=encryption_context, materials_manager=cmm, + ) + except MissingClassificationError: + # Your encryption context did not contain a classification identifier. + # Reaching this point means everything is working as expected. + pass + else: + # The classification requiring CMM keeps this from happening. + raise AssertionError("The classification requiring CMM does not let this happen!") + + # Encrypt your plaintext data. + classified_ciphertext, _encrypt_header = aws_encryption_sdk.encrypt( + source=source_plaintext, + encryption_context=dict(classification="secret", **encryption_context), + materials_manager=cmm, + ) + + # Demonstrate that the ciphertext and plaintext are different. + assert classified_ciphertext != source_plaintext + + # Decrypt your encrypted data using the same cryptographic materials manager 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=classified_ciphertext, materials_manager=cmm) + + # 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()) + + # Now demonstrate the decrypt path of the classification requiring cryptographic materials manager. + + # Encrypt your plaintext using the keyring and do not include a classification identifier. + unclassified_ciphertext, encrypt_header = aws_encryption_sdk.encrypt( + source=source_plaintext, encryption_context=encryption_context, keyring=keyring + ) + + assert "classification" not in encrypt_header.encryption_context + + # Demonstrate that the classification requiring CMM + # will not let you decrypt messages without classification identifiers. + try: + aws_encryption_sdk.decrypt(source=unclassified_ciphertext, materials_manager=cmm) + except MissingClassificationError: + # Your encryption context did not contain a classification identifier. + # Reaching this point means everything is working as expected. + pass + else: + # The classification requiring CMM keeps this from happening. + raise AssertionError("The classification requiring CMM does not let this happen!") diff --git a/examples/src/file_streaming_defaults.py b/examples/src/file_streaming_defaults.py new file mode 100644 index 000000000..84bba1d31 --- /dev/null +++ b/examples/src/file_streaming_defaults.py @@ -0,0 +1,82 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +This example shows how to use the streaming encrypt and decrypt APIs when working with files. + +One benefit of using the streaming API is that +we can check the encryption context in the header before we start decrypting. + +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 ``master_key_provider`` directories. +""" +import filecmp + +import aws_encryption_sdk +from aws_encryption_sdk.keyrings.aws_kms import KmsKeyring + + +def run(aws_kms_cmk, source_plaintext_filename): + # type: (str, str) -> None + """Demonstrate an encrypt/decrypt cycle using the streaming encrypt/decrypt APIs with files. + + :param str aws_kms_cmk: The ARN of an AWS KMS CMK that protects data keys + :param str source_plaintext_filename: Path to plaintext file to encrypt + """ + # We assume that you can also write to the directory containing the plaintext file, + # so that is where we will put all of the results. + ciphertext_filename = source_plaintext_filename + ".encrypted" + decrypted_filename = ciphertext_filename + ".decrypted" + + # 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) + + # Open the files you want to work with. + with open(source_plaintext_filename, "rb") as plaintext, open(ciphertext_filename, "wb") as ciphertext: + # The streaming API provides a context manager. + # You can read from it just as you read from a file. + with aws_encryption_sdk.stream( + mode="encrypt", source=plaintext, encryption_context=encryption_context, keyring=keyring + ) as encryptor: + # Iterate through the segments in the context manager + # and write the results to the ciphertext. + for segment in encryptor: + ciphertext.write(segment) + + # 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. + # + # 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. + # + # 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(decryptor.header.encryption_context.items()) + + # Now that we are more confident that we will decrypt the right message, + # we can start decrypting. + for segment in decryptor: + decrypted.write(segment) + + # 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 new file mode 100644 index 000000000..984848bce --- /dev/null +++ b/examples/src/in_memory_streaming_defaults.py @@ -0,0 +1,79 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +This example shows how to use the streaming encrypt and decrypt APIs on data in memory. + +One benefit of using the streaming API is that +we can check the encryption context in the header before we start decrypting. + +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 ``master_key_provider`` directories. +""" +import io + +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 the streaming encrypt/decrypt APIs in-memory. + + :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) + + ciphertext = io.BytesIO() + + # The streaming API provides a context manager. + # You can read from it just as you read from a file. + with aws_encryption_sdk.stream( + mode="encrypt", source=source_plaintext, encryption_context=encryption_context, keyring=keyring + ) as encryptor: + # Iterate through the segments in the context manager + # and write the results to the ciphertext. + for segment in encryptor: + ciphertext.write(segment) + + # 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. + ciphertext.seek(0) + + # 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 = 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. + # + # 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(decryptor.header.encryption_context.items()) + + # Now that we are more confident that we will decrypt the right message, + # we can start decrypting. + for segment in decryptor: + decrypted.write(segment) + + # 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..b90d8ccc3 --- /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, 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 "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(key_ids=[aws_kms_generator_cmk]) + single_cmk_keyring_that_encrypted = KmsKeyring(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..5abe28ea4 --- /dev/null +++ b/examples/src/keyring/raw_aes/raw_aes.py @@ -0,0 +1,72 @@ +# 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.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", + } + + # Generate a 256-bit (32 byte) AES key to use with your keyring. + # + # In practice, you should get this key from a secure key management system such as an HSM. + key = os.urandom(32) + + # 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 AES 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, + ) + + # 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/__init__.py b/examples/src/legacy/__init__.py new file mode 100644 index 000000000..e4646257c --- /dev/null +++ b/examples/src/legacy/__init__.py @@ -0,0 +1,9 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Legacy examples. + +We keep these older examples as reference material, +but we recommend that you use the new examples. +The new examples reflect our current guidance for using the library. +""" diff --git a/examples/src/basic_encryption.py b/examples/src/legacy/basic_encryption.py similarity index 63% rename from examples/src/basic_encryption.py rename to examples/src/legacy/basic_encryption.py index 6c194e45d..81db7a102 100644 --- a/examples/src/basic_encryption.py +++ b/examples/src/legacy/basic_encryption.py @@ -1,29 +1,19 @@ -# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). You -# may not use this file except in compliance with the License. A copy of -# the License is located at -# -# http://aws.amazon.com/apache2.0/ -# -# or in the "license" file accompanying this file. This file is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF -# ANY KIND, either express or implied. See the License for the specific -# language governing permissions and limitations under the License. -"""Example showing basic encryption and decryption of a value already in memory.""" +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Example showing how to encrypt and decrypt a value in memory.""" import aws_encryption_sdk -def cycle_string(key_arn, source_plaintext, botocore_session=None): +def run(aws_kms_cmk, source_plaintext, botocore_session=None): """Encrypts and then decrypts a string under a KMS customer master key (CMK). - :param str key_arn: Amazon Resource Name (ARN) of the KMS CMK + :param str aws_kms_cmk: Amazon Resource Name (ARN) of the AWS KMS CMK :param bytes source_plaintext: Data to encrypt :param botocore_session: existing botocore session instance :type botocore_session: botocore.session.Session """ # Create a KMS master key provider - kms_kwargs = dict(key_ids=[key_arn]) + kms_kwargs = dict(key_ids=[aws_kms_cmk]) if botocore_session is not None: kms_kwargs["botocore_session"] = botocore_session master_key_provider = aws_encryption_sdk.KMSMasterKeyProvider(**kms_kwargs) diff --git a/examples/src/basic_file_encryption_with_multiple_providers.py b/examples/src/legacy/basic_file_encryption_with_multiple_providers.py similarity index 88% rename from examples/src/basic_file_encryption_with_multiple_providers.py rename to examples/src/legacy/basic_file_encryption_with_multiple_providers.py index 9edceef9e..1319025d9 100644 --- a/examples/src/basic_file_encryption_with_multiple_providers.py +++ b/examples/src/legacy/basic_file_encryption_with_multiple_providers.py @@ -1,15 +1,5 @@ -# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). You -# may not use this file except in compliance with the License. A copy of -# the License is located at -# -# http://aws.amazon.com/apache2.0/ -# -# or in the "license" file accompanying this file. This file is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF -# ANY KIND, either express or implied. See the License for the specific -# language governing permissions and limitations under the License. +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 """Example showing creation of a RawMasterKeyProvider, how to use multiple master key providers to encrypt, and demonstrating that each master key provider can then be used independently to decrypt the same encrypted message. @@ -60,12 +50,12 @@ def _get_raw_key(self, key_id): ) -def cycle_file(key_arn, source_plaintext_filename, botocore_session=None): +def run(aws_kms_cmk, source_plaintext_filename, botocore_session=None): """Encrypts and then decrypts a file using a KMS master key provider and a custom static master key provider. Both master key providers are used to encrypt the plaintext file, so either one alone can decrypt it. - :param str key_arn: Amazon Resource Name (ARN) of the KMS Customer Master Key (CMK) + :param str aws_kms_cmk: Amazon Resource Name (ARN) of the KMS Customer Master Key (CMK) (http://docs.aws.amazon.com/kms/latest/developerguide/viewing-keys.html) :param str source_plaintext_filename: Filename of file to encrypt :param botocore_session: existing botocore session instance @@ -77,7 +67,7 @@ def cycle_file(key_arn, source_plaintext_filename, botocore_session=None): cycled_static_plaintext_filename = source_plaintext_filename + ".static.decrypted" # Create a KMS master key provider - kms_kwargs = dict(key_ids=[key_arn]) + kms_kwargs = dict(key_ids=[aws_kms_cmk]) if botocore_session is not None: kms_kwargs["botocore_session"] = botocore_session kms_master_key_provider = aws_encryption_sdk.KMSMasterKeyProvider(**kms_kwargs) diff --git a/examples/src/basic_file_encryption_with_raw_key_provider.py b/examples/src/legacy/basic_file_encryption_with_raw_key_provider.py similarity index 83% rename from examples/src/basic_file_encryption_with_raw_key_provider.py rename to examples/src/legacy/basic_file_encryption_with_raw_key_provider.py index 91e2a7e9a..1e3eff8e0 100644 --- a/examples/src/basic_file_encryption_with_raw_key_provider.py +++ b/examples/src/legacy/basic_file_encryption_with_raw_key_provider.py @@ -1,15 +1,5 @@ -# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). You -# may not use this file except in compliance with the License. A copy of -# the License is located at -# -# http://aws.amazon.com/apache2.0/ -# -# or in the "license" file accompanying this file. This file is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF -# ANY KIND, either express or implied. See the License for the specific -# language governing permissions and limitations under the License. +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 """Example showing creation and use of a RawMasterKeyProvider.""" import filecmp import os @@ -48,7 +38,7 @@ def _get_raw_key(self, key_id): ) -def cycle_file(source_plaintext_filename): +def run(source_plaintext_filename): """Encrypts and then decrypts a file under a custom static master key provider. :param str source_plaintext_filename: Filename of file to encrypt diff --git a/examples/src/data_key_caching_basic.py b/examples/src/legacy/data_key_caching_basic.py similarity index 67% rename from examples/src/data_key_caching_basic.py rename to examples/src/legacy/data_key_caching_basic.py index 1d5445615..899aed294 100644 --- a/examples/src/data_key_caching_basic.py +++ b/examples/src/legacy/data_key_caching_basic.py @@ -1,23 +1,13 @@ -# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). You -# may not use this file except in compliance with the License. A copy of -# the License is located at -# -# http://aws.amazon.com/apache2.0/ -# -# or in the "license" file accompanying this file. This file is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF -# ANY KIND, either express or implied. See the License for the specific -# language governing permissions and limitations under the License. +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 """Example of encryption with data key caching.""" import aws_encryption_sdk -def encrypt_with_caching(kms_cmk_arn, max_age_in_cache, cache_capacity): +def run(aws_kms_cmk, max_age_in_cache=10.0, cache_capacity=10): """Encrypts a string using an AWS KMS customer master key (CMK) and data key caching. - :param str kms_cmk_arn: Amazon Resource Name (ARN) of the KMS customer master key + :param str aws_kms_cmk: Amazon Resource Name (ARN) of the KMS customer master key :param float max_age_in_cache: Maximum time in seconds that a cached entry can be used :param int cache_capacity: Maximum number of entries to retain in cache at once """ @@ -32,7 +22,7 @@ def encrypt_with_caching(kms_cmk_arn, max_age_in_cache, cache_capacity): encryption_context = {"purpose": "test"} # Create a master key provider for the KMS customer master key (CMK) - key_provider = aws_encryption_sdk.KMSMasterKeyProvider(key_ids=[kms_cmk_arn]) + key_provider = aws_encryption_sdk.KMSMasterKeyProvider(key_ids=[aws_kms_cmk]) # Create a local cache cache = aws_encryption_sdk.LocalCryptoMaterialsCache(cache_capacity) diff --git a/examples/src/one_kms_cmk_streaming_data.py b/examples/src/legacy/one_kms_cmk_streaming_data.py similarity index 72% rename from examples/src/one_kms_cmk_streaming_data.py rename to examples/src/legacy/one_kms_cmk_streaming_data.py index 3edfa82ab..90854398d 100644 --- a/examples/src/one_kms_cmk_streaming_data.py +++ b/examples/src/legacy/one_kms_cmk_streaming_data.py @@ -1,32 +1,22 @@ -# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). You -# may not use this file except in compliance with the License. A copy of -# the License is located at -# -# http://aws.amazon.com/apache2.0/ -# -# or in the "license" file accompanying this file. This file is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF -# ANY KIND, either express or implied. See the License for the specific -# language governing permissions and limitations under the License. +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 """Example showing basic encryption and decryption of streaming data in memory using one KMS CMK.""" import filecmp import aws_encryption_sdk -def encrypt_decrypt_stream(key_arn, source_plaintext_filename, botocore_session=None): +def run(aws_kms_cmk, source_plaintext_filename, botocore_session=None): """Encrypts and then decrypts streaming data under one KMS customer master key (CMK). - :param str key_arn: Amazon Resource Name (ARN) of the KMS CMK + :param str aws_kms_cmk: Amazon Resource Name (ARN) of the KMS CMK :param str source_plaintext_filename: Filename of file to encrypt :param botocore_session: existing botocore session instance :type botocore_session: botocore.session.Session """ kwargs = dict() - kwargs["key_ids"] = [key_arn] + kwargs["key_ids"] = [aws_kms_cmk] if botocore_session is not None: kwargs["botocore_session"] = botocore_session diff --git a/examples/src/one_kms_cmk_unsigned.py b/examples/src/legacy/one_kms_cmk_unsigned.py similarity index 69% rename from examples/src/one_kms_cmk_unsigned.py rename to examples/src/legacy/one_kms_cmk_unsigned.py index df2f4373d..a127261b7 100644 --- a/examples/src/one_kms_cmk_unsigned.py +++ b/examples/src/legacy/one_kms_cmk_unsigned.py @@ -1,15 +1,5 @@ -# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). You -# may not use this file except in compliance with the License. A copy of -# the License is located at -# -# http://aws.amazon.com/apache2.0/ -# -# or in the "license" file accompanying this file. This file is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF -# ANY KIND, either express or implied. See the License for the specific -# language governing permissions and limitations under the License. +# 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 with an unsigned algorithm. """ @@ -17,15 +7,15 @@ from aws_encryption_sdk.identifiers import Algorithm -def encrypt_decrypt(key_arn, source_plaintext, botocore_session=None): +def run(aws_kms_cmk, source_plaintext, botocore_session=None): """Encrypts and then decrypts a string under one KMS customer master key (CMK) with an unsigned algorithm. - :param str key_arn: Amazon Resource Name (ARN) of the KMS 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=[key_arn]) + kwargs = dict(key_ids=[aws_kms_cmk]) if botocore_session is not None: kwargs["botocore_session"] = botocore_session diff --git a/examples/src/master_key_provider/__init__.py b/examples/src/master_key_provider/__init__.py new file mode 100644 index 000000000..0edf699c5 --- /dev/null +++ b/examples/src/master_key_provider/__init__.py @@ -0,0 +1,7 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Master key provider examples. + +These examples show how to use master key providers. +""" diff --git a/examples/src/master_key_provider/aws_kms/__init__.py b/examples/src/master_key_provider/aws_kms/__init__.py new file mode 100644 index 000000000..5da30d818 --- /dev/null +++ b/examples/src/master_key_provider/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 master key provider examples. + +These examples show how to use the KMS master key provider. +""" diff --git a/examples/src/master_key_provider/aws_kms/discovery_decrypt.py b/examples/src/master_key_provider/aws_kms/discovery_decrypt.py new file mode 100644 index 000000000..1b7e09452 --- /dev/null +++ b/examples/src/master_key_provider/aws_kms/discovery_decrypt.py @@ -0,0 +1,71 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +This example is intended to serve as reference material for users migrating away from master key providers. +We recommend using keyrings rather than master key providers. +For examples using keyrings, see the ``examples/src/keyrings`` directory. + +The KMS master key provider uses any key IDs that you specify on encrypt, +but attempts to decrypt *any* data keys that were encrypted under a KMS CMK. +This means that you do not need to know which CMKs were used to encrypt a message. + +This example shows how to configure and use a KMS master key provider to decrypt without provider key IDs. + +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#master-key-provider + +For an example of how to use the KMS master key with a single CMK, +see the ``master_key_provider/aws_kms/single_cmk`` example. + +For an example of how to use the KMS master key provider with CMKs in multiple regions, +see the ``master_key_provider/aws_kms/multiple_regions`` example. +""" +import aws_encryption_sdk +from aws_encryption_sdk.key_providers.kms import KMSMasterKey, KMSMasterKeyProvider + + +def run(aws_kms_cmk, source_plaintext): + # type: (str, bytes) -> None + """Demonstrate configuring a KMS master key provider 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 master key that determines how your data keys are protected. + encrypt_master_key = KMSMasterKey(key_id=aws_kms_cmk) + + # Create a KMS master key provider to use on decrypt. + decrypt_master_key_provider = KMSMasterKeyProvider() + + # Encrypt your plaintext data. + ciphertext, _encrypt_header = aws_encryption_sdk.encrypt( + source=source_plaintext, encryption_context=encryption_context, key_provider=encrypt_master_key + ) + + # Demonstrate that the ciphertext and plaintext are different. + assert ciphertext != source_plaintext + + # Decrypt your encrypted data using the KMS master key provider. + # + # 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, key_provider=decrypt_master_key_provider) + + # 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/master_key_provider/aws_kms/multiple_regions.py b/examples/src/master_key_provider/aws_kms/multiple_regions.py new file mode 100644 index 000000000..81fee9ec5 --- /dev/null +++ b/examples/src/master_key_provider/aws_kms/multiple_regions.py @@ -0,0 +1,91 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +This example is intended to serve as reference material for users migrating away from master key providers. +We recommend using keyrings rather than master key providers. +For examples using keyrings, see the ``examples/src/keyrings`` directory. + +This example shows how to configure and use a KMS master key provider with with CMKs in multiple regions. + +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#master-key-provider + +For an example of how to use the KMS master key with a single CMK, +see the ``master_key_provider/aws_kms/single_cmk`` example. + +For an example of how to use the KMS master key provider in discovery mode on decrypt, +see the ``master_key_provider/aws_kms/discovery_decrypt`` example. +""" +import aws_encryption_sdk +from aws_encryption_sdk.key_providers.kms import KMSMasterKey, KMSMasterKeyProvider + +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 master key provider 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 master key provider that will encrypt your data keys under all requested CMKs. + # + # The KMS master key provider generates the data key using the first key ID in the list. + key_ids = [aws_kms_generator_cmk] + key_ids.extend(aws_kms_additional_cmks) + master_key_provider = KMSMasterKeyProvider(key_ids=key_ids) + + # Create master keys 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. + single_cmk_master_key_that_generated = KMSMasterKey(key_id=aws_kms_generator_cmk) + single_cmk_master_key_that_encrypted = KMSMasterKey(key_id=aws_kms_additional_cmks[0]) + + # Encrypt your plaintext data using the master key provider that uses all requests CMKs. + ciphertext, encrypt_header = aws_encryption_sdk.encrypt( + source=source_plaintext, encryption_context=encryption_context, key_provider=master_key_provider + ) + + # 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 master keys. + # + # 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, key_provider=single_cmk_master_key_that_generated + ) + decrypted_2, decrypt_header_2 = aws_encryption_sdk.decrypt( + source=ciphertext, key_provider=single_cmk_master_key_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/master_key_provider/aws_kms/single_cmk.py b/examples/src/master_key_provider/aws_kms/single_cmk.py new file mode 100644 index 000000000..5a23493d6 --- /dev/null +++ b/examples/src/master_key_provider/aws_kms/single_cmk.py @@ -0,0 +1,64 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +This example is intended to serve as reference material for users migrating away from master key providers. +We recommend using keyrings rather than master key providers. +For examples using keyrings, see the ``examples/src/keyrings`` directory. + +This example shows how to configure and use a KMS master key with a single KMS CMK. + +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#master-key-provider + +For an example of how to use the KMS master key provider with CMKs in multiple regions, +see the ``master_key_provider/aws_kms/multiple_regions`` example. + +For an example of how to use the KMS master key provider in discovery mode on decrypt, +see the ``master_key_provider/aws_kms/discovery_decrypt`` example. +""" +import aws_encryption_sdk +from aws_encryption_sdk.key_providers.kms import KMSMasterKey + + +def run(aws_kms_cmk, source_plaintext): + # type: (str, bytes) -> None + """Demonstrate an encrypt/decrypt cycle using a KMS master key 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 master key that determines how your data keys are protected. + master_key = KMSMasterKey(key_id=aws_kms_cmk) + + # Encrypt your plaintext data. + ciphertext, _encrypt_header = aws_encryption_sdk.encrypt( + source=source_plaintext, encryption_context=encryption_context, key_provider=master_key + ) + + # Demonstrate that the ciphertext and plaintext are different. + assert ciphertext != source_plaintext + + # Decrypt your encrypted data using the same master key 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, key_provider=master_key) + + # 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/master_key_provider/multi/__init__.py b/examples/src/master_key_provider/multi/__init__.py new file mode 100644 index 000000000..22f5195fc --- /dev/null +++ b/examples/src/master_key_provider/multi/__init__.py @@ -0,0 +1,7 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Multi-master key provider examples. + +These examples show how to combine master key providers. +""" diff --git a/examples/src/master_key_provider/multi/aws_kms_with_escrow.py b/examples/src/master_key_provider/multi/aws_kms_with_escrow.py new file mode 100644 index 000000000..a9c4a2130 --- /dev/null +++ b/examples/src/master_key_provider/multi/aws_kms_with_escrow.py @@ -0,0 +1,150 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +This example is intended to serve as reference material for users migrating away from master key providers. +We recommend using keyrings rather than master key providers. +For examples using keyrings, see the ``examples/src/keyrings`` directory. + +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 achieve this +by combining a KMS master key with a raw RSA master key. + +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#master-key-provider + +For more examples of how to use the KMS master key provider, see the ``master_key_provider/aws_kms`` examples. + +For more examples of how to use the raw RSA master key, see the ``master_key_provider/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 import serialization +from cryptography.hazmat.primitives.asymmetric import rsa + +import aws_encryption_sdk +from aws_encryption_sdk.identifiers import EncryptionKeyType, WrappingAlgorithm +from aws_encryption_sdk.key_providers.kms import KMSMasterKeyProvider +from aws_encryption_sdk.key_providers.raw import RawMasterKey, WrappingKey + + +def run(aws_kms_cmk, source_plaintext): + # type: (str, bytes) -> None + """Demonstrate configuring a master key provider 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 master key. + # 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(), + ) + + # Collect the public key from the private key. + public_key = private_key.public_key() + + # Serialize the RSA public key to PEM encoding. + # This or DER encoding is likely to be what you get from your key management system in practice. + public_key_pem = public_key.public_bytes( + encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo, + ) + + # Create the encrypt master key that only has access to the public key. + escrow_encrypt_master_key = RawMasterKey( + # The provider ID and key ID are defined by you + # and are used by the raw RSA master key + # to determine whether it should attempt to decrypt + # an encrypted data key. + provider_id="some managed raw keys", # provider ID corresponds to key namespace for keyrings + key_id=b"my RSA wrapping key", # key ID corresponds to key name for keyrings + wrapping_key=WrappingKey( + wrapping_key=public_key_pem, + wrapping_key_type=EncryptionKeyType.PUBLIC, + # The wrapping algorithm tells the raw RSA master key + # 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 master key that has access to the private key. + escrow_decrypt_master_key = RawMasterKey( + # The key namespace and key name MUST match the encrypt master key. + provider_id="some managed raw keys", # provider ID corresponds to key namespace for keyrings + key_id=b"my RSA wrapping key", # key ID corresponds to key name for keyrings + wrapping_key=WrappingKey( + wrapping_key=private_key_pem, + wrapping_key_type=EncryptionKeyType.PRIVATE, + # The wrapping algorithm MUST match the encrypt master key. + wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, + ), + ) + + # Create the KMS master key that you will use for decryption during normal operations. + kms_master_key = KMSMasterKeyProvider(key_ids=[aws_kms_cmk]) + + # Add the escrow encrypt master key to the KMS master key. + kms_master_key.add_master_key_provider(escrow_encrypt_master_key) + + # Encrypt your plaintext data using the combined master keys. + ciphertext, encrypt_header = aws_encryption_sdk.encrypt( + source=source_plaintext, encryption_context=encryption_context, key_provider=kms_master_key + ) + + # 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 master key and the escrow decrypt master key. + # + # 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, key_provider=kms_master_key) + decrypted_escrow, decrypt_header_escrow = aws_encryption_sdk.decrypt( + source=ciphertext, key_provider=escrow_decrypt_master_key + ) + + # 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/master_key_provider/raw_aes/__init__.py b/examples/src/master_key_provider/raw_aes/__init__.py new file mode 100644 index 000000000..5572015a7 --- /dev/null +++ b/examples/src/master_key_provider/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 master key provider examples. + +These examples show how to use the raw AES master key. +""" diff --git a/examples/src/master_key_provider/raw_aes/raw_aes.py b/examples/src/master_key_provider/raw_aes/raw_aes.py new file mode 100644 index 000000000..943fcb67b --- /dev/null +++ b/examples/src/master_key_provider/raw_aes/raw_aes.py @@ -0,0 +1,81 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +This example is intended to serve as reference material for users migrating away from master key providers. +We recommend using keyrings rather than master key providers. +For examples using keyrings, see the ``examples/src/keyrings`` directory. + +This examples shows how to configure and use a raw AES master key. + +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#master-key-provider + +In this example, we use the one-step encrypt and decrypt APIs. +""" +import os + +import aws_encryption_sdk +from aws_encryption_sdk.identifiers import EncryptionKeyType, WrappingAlgorithm +from aws_encryption_sdk.key_providers.raw import RawMasterKey, WrappingKey + + +def run(source_plaintext): + # type: (bytes) -> None + """Demonstrate an encrypt/decrypt cycle using a raw AES master 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", + } + + # Choose the wrapping algorithm for your master key to use. + wrapping_algorithm = WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING + + # Generate an AES key to use with your master key. + # 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 master key that determines how your data keys are protected. + master_key = RawMasterKey( + # The provider ID and key ID are defined by you + # and are used by the raw AES master key + # to determine whether it should attempt to decrypt + # an encrypted data key. + provider_id="some managed raw keys", # provider ID corresponds to key namespace for keyrings + key_id=b"my AES wrapping key", # key ID corresponds to key name for keyrings + wrapping_key=WrappingKey( + wrapping_algorithm=wrapping_algorithm, wrapping_key_type=EncryptionKeyType.SYMMETRIC, wrapping_key=key, + ), + ) + + # Encrypt your plaintext data. + ciphertext, _encrypt_header = aws_encryption_sdk.encrypt( + source=source_plaintext, encryption_context=encryption_context, key_provider=master_key + ) + + # Demonstrate that the ciphertext and plaintext are different. + assert ciphertext != source_plaintext + + # Decrypt your encrypted data using the same master key 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, key_provider=master_key) + + # 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/master_key_provider/raw_rsa/__init__.py b/examples/src/master_key_provider/raw_rsa/__init__.py new file mode 100644 index 000000000..374a606fb --- /dev/null +++ b/examples/src/master_key_provider/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 master key provider examples. + +These examples show how to use the raw RSA master key. +""" diff --git a/examples/src/master_key_provider/raw_rsa/private_key_only_from_pem.py b/examples/src/master_key_provider/raw_rsa/private_key_only_from_pem.py new file mode 100644 index 000000000..5ad78c00b --- /dev/null +++ b/examples/src/master_key_provider/raw_rsa/private_key_only_from_pem.py @@ -0,0 +1,104 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +This example is intended to serve as reference material for users migrating away from master key providers. +We recommend using keyrings rather than master key providers. +For examples using keyrings, see the ``examples/src/keyrings`` directory. + +This example shows how to configure and use a raw RSA master key using a PEM-encoded RSA private key. + +The most commonly used encodings for RSA keys tend to be PEM and DER. +The raw RSA master key supports loading both public and private keys from PEM encoding. + +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#master-key-provider + +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 EncryptionKeyType, WrappingAlgorithm +from aws_encryption_sdk.key_providers.raw import RawMasterKey, WrappingKey + + +def run(source_plaintext): + # type: (bytes) -> None + """Demonstrate an encrypt/decrypt cycle using a raw RSA master key 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 master key. + # 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 master key that determines how your data keys are protected. + # + # WrappingKey can only load PEM-encoded keys. + master_key = RawMasterKey( + # The provider ID and key ID are defined by you + # and are used by the raw RSA master key + # to determine whether it should attempt to decrypt + # an encrypted data key. + provider_id="some managed raw keys", # provider ID corresponds to key namespace for keyrings + key_id=b"my RSA wrapping key", # key ID corresponds to key name for keyrings + wrapping_key=WrappingKey( + wrapping_key=private_key_pem, + wrapping_key_type=EncryptionKeyType.PRIVATE, + # The wrapping algorithm tells the raw RSA master key + # 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, key_provider=master_key + ) + + # Demonstrate that the ciphertext and plaintext are different. + assert ciphertext != source_plaintext + + # Decrypt your encrypted data using the same master key 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, key_provider=master_key) + + # 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/one_kms_cmk.py b/examples/src/one_kms_cmk.py deleted file mode 100644 index 1ba1d869f..000000000 --- a/examples/src/one_kms_cmk.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). You -# may not use this file except in compliance with the License. A copy of -# the License is located at -# -# http://aws.amazon.com/apache2.0/ -# -# or in the "license" file accompanying this file. This file is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF -# ANY KIND, either express or implied. See the License for the specific -# language governing permissions and limitations under the License. -"""Example showing basic encryption and decryption of a value already in memory using one KMS CMK.""" -import aws_encryption_sdk - - -def encrypt_decrypt(key_arn, source_plaintext, botocore_session=None): - """Encrypts and then decrypts a string under one KMS customer master key (CMK). - - :param str key_arn: 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=[key_arn]) - - 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 new file mode 100644 index 000000000..3ded2b3ae --- /dev/null +++ b/examples/src/onestep_defaults.py @@ -0,0 +1,57 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +This example shows how to use the one-step encrypt and decrypt APIs. + +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 ``master_key_provider`` directories. +""" +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 the one-step encrypt/decrypt APIs. + + :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/onestep_unsigned.py b/examples/src/onestep_unsigned.py new file mode 100644 index 000000000..72623087b --- /dev/null +++ b/examples/src/onestep_unsigned.py @@ -0,0 +1,77 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +This example shows how to specify an algorithm suite +when using the one-step encrypt and decrypt APIs. + +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 ``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 +for a wrapping key that you used when encrypting a message +under multiple wrapping keys. + +However, if all of your readers and writers have the same permissions, +then this additional protection does not always add value. +This example shows you how to select another algorithm suite +that has all of the other properties of the default suite +but does not include a message-level signature. +""" +import aws_encryption_sdk +from aws_encryption_sdk.identifiers import AlgorithmSuite +from aws_encryption_sdk.keyrings.aws_kms import KmsKeyring + + +def run(aws_kms_cmk, source_plaintext): + # type: (str, bytes) -> None + """Demonstrate requesting a specific algorithm suite through the one-step encrypt/decrypt APIs. + + :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, + # Here we can specify the algorithm suite that we want to use. + algorithm=AlgorithmSuite.AES_256_GCM_IV12_TAG16_HKDF_SHA256, + ) + + # 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. + # + # 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) + + # 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/test/examples_test_utils.py b/examples/test/examples_test_utils.py index 0984ee684..49379ff1b 100644 --- a/examples/test/examples_test_utils.py +++ b/examples/test/examples_test_utils.py @@ -1,21 +1,33 @@ -# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). You -# may not use this file except in compliance with the License. A copy of -# the License is located at -# -# http://aws.amazon.com/apache2.0/ -# -# or in the "license" file accompanying this file. This file is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF -# ANY KIND, either express or implied. See the License for the specific -# language governing permissions and limitations under the License. +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 """Helper utilities for use while testing examples.""" import os import sys +import inspect + +import pytest +import six + +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Callable, Dict, Iterable, List # noqa pylint: disable=unused-import + + # we only need pathlib here for typehints + from pathlib import Path +except ImportError: # pragma: no cover + # We only actually need these imports when running the mypy checks + pass + +HERE = os.path.abspath(os.path.dirname(__file__)) +EXAMPLES_SOURCE = os.path.join(HERE, "..", "src") +SINGLE_CMK_ARG = "aws_kms_cmk" +GENERATOR_CMK_ARG = "aws_kms_generator_cmk" +ADDITIONAL_CMKS_ARG = "aws_kms_additional_cmks" +PLAINTEXT_ARG = "source_plaintext" +PLAINTEXT_FILE_ARG = "source_plaintext_filename" 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. " @@ -47,4 +59,62 @@ ) -from integration_test_utils import get_cmk_arn # noqa pylint: disable=unused-import,import-error +def all_examples(): + # type: () -> Iterable[pytest.param] + for (dirpath, _dirnames, filenames) in os.walk(EXAMPLES_SOURCE): + for testfile in filenames: + split_path = testfile.rsplit(".", 1) + if len(split_path) != 2: + continue + stem, suffix = split_path + if suffix == "py" and stem != "__init__": + 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) + else: + import_path = "..src.{name}".format(name=module_name) + + yield pytest.param(import_path, id="{base}.{name}".format(base=module_parent, name=module_name)) + + +def get_arg_names(function): + # type: (Callable) -> List[str] + if six.PY2: + # getargspec was deprecated in CPython 3.0 but 2.7 does not have either of the new options + spec = inspect.getargspec(function) # pylint: disable=deprecated-method + return spec.args + + spec = inspect.getfullargspec(function) + return spec.args + + +def build_kwargs(function, temp_dir): + # type: (Callable, Path) -> Dict[str, str] + + plaintext_file = temp_dir / "plaintext" + plaintext_file.write_bytes(static_plaintext) + + cmk_arns = get_all_cmk_arns() + + args = get_arg_names(function) + possible_kwargs = { + SINGLE_CMK_ARG: cmk_arns[0], + GENERATOR_CMK_ARG: cmk_arns[0], + ADDITIONAL_CMKS_ARG: cmk_arns[1:], + PLAINTEXT_ARG: static_plaintext, + PLAINTEXT_FILE_ARG: str(plaintext_file.absolute()), + } + kwargs = {} + for name in args: + try: + kwargs[name] = possible_kwargs[name] + 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_i_basic_encryption.py b/examples/test/test_i_basic_encryption.py deleted file mode 100644 index f2a4fab51..000000000 --- a/examples/test/test_i_basic_encryption.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright 2017-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). You -# may not use this file except in compliance with the License. A copy of -# the License is located at -# -# http://aws.amazon.com/apache2.0/ -# -# or in the "license" file accompanying this file. This file is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF -# ANY KIND, either express or implied. See the License for the specific -# language governing permissions and limitations under the License. -"""Unit test suite for the Strings examples in the AWS-hosted documentation.""" -import botocore.session -import pytest - -from ..src.basic_encryption import cycle_string -from .examples_test_utils import get_cmk_arn, static_plaintext - - -pytestmark = [pytest.mark.examples] - - -def test_cycle_string(): - plaintext = static_plaintext - cmk_arn = get_cmk_arn() - cycle_string(key_arn=cmk_arn, source_plaintext=plaintext, botocore_session=botocore.session.Session()) diff --git a/examples/test/test_i_basic_file_encryption_with_multiple_providers.py b/examples/test/test_i_basic_file_encryption_with_multiple_providers.py deleted file mode 100644 index 282a272ab..000000000 --- a/examples/test/test_i_basic_file_encryption_with_multiple_providers.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright 2017-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). You -# may not use this file except in compliance with the License. A copy of -# the License is located at -# -# http://aws.amazon.com/apache2.0/ -# -# or in the "license" file accompanying this file. This file is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF -# ANY KIND, either express or implied. See the License for the specific -# language governing permissions and limitations under the License. -"""Unit test suite for the Bytes Streams Multiple Providers examples in the AWS-hosted documentation.""" -import os -import tempfile - -import botocore.session -import pytest - -from ..src.basic_file_encryption_with_multiple_providers import cycle_file -from .examples_test_utils import get_cmk_arn -from .examples_test_utils import static_plaintext - - -pytestmark = [pytest.mark.examples] - - -def test_cycle_file(): - cmk_arn = get_cmk_arn() - handle, filename = tempfile.mkstemp() - with open(filename, "wb") as f: - f.write(static_plaintext) - try: - new_files = cycle_file( - key_arn=cmk_arn, source_plaintext_filename=filename, botocore_session=botocore.session.Session() - ) - for f in new_files: - os.remove(f) - finally: - os.close(handle) - os.remove(filename) diff --git a/examples/test/test_i_basic_file_encryption_with_raw_key_provider.py b/examples/test/test_i_basic_file_encryption_with_raw_key_provider.py deleted file mode 100644 index 710c0ccac..000000000 --- a/examples/test/test_i_basic_file_encryption_with_raw_key_provider.py +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright 2017-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). You -# may not use this file except in compliance with the License. A copy of -# the License is located at -# -# http://aws.amazon.com/apache2.0/ -# -# or in the "license" file accompanying this file. This file is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF -# ANY KIND, either express or implied. See the License for the specific -# language governing permissions and limitations under the License. -"""Unit test suite for the Bytes Streams examples in the AWS-hosted documentation.""" -import os -import tempfile - -import pytest - -from ..src.basic_file_encryption_with_raw_key_provider import cycle_file -from .examples_test_utils import static_plaintext - - -pytestmark = [pytest.mark.examples] - - -def test_cycle_file(): - handle, filename = tempfile.mkstemp() - with open(filename, "wb") as f: - f.write(static_plaintext) - try: - new_files = cycle_file(source_plaintext_filename=filename) - for f in new_files: - os.remove(f) - finally: - os.close(handle) - os.remove(filename) diff --git a/examples/test/test_i_one_kms_cmk_streaming_data.py b/examples/test/test_i_one_kms_cmk_streaming_data.py deleted file mode 100644 index b22fa4232..000000000 --- a/examples/test/test_i_one_kms_cmk_streaming_data.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright 2017-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). You -# may not use this file except in compliance with the License. A copy of -# the License is located at -# -# http://aws.amazon.com/apache2.0/ -# -# or in the "license" file accompanying this file. This file is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF -# ANY KIND, either express or implied. See the License for the specific -# language governing permissions and limitations under the License. -"""Unit test suite for the encryption and decryption of streaming data using one KMS CMK example.""" -import os -import tempfile - -import botocore.session -import pytest - -from ..src.one_kms_cmk_streaming_data import encrypt_decrypt_stream -from .examples_test_utils import get_cmk_arn, static_plaintext - - -pytestmark = [pytest.mark.examples] - - -def test_one_kms_cmk_streaming_data(): - cmk_arn = get_cmk_arn() - handle, filename = tempfile.mkstemp() - with open(filename, "wb") as f: - f.write(static_plaintext) - try: - new_files = encrypt_decrypt_stream( - key_arn=cmk_arn, source_plaintext_filename=filename, botocore_session=botocore.session.Session() - ) - for f in new_files: - os.remove(f) - finally: - os.close(handle) - os.remove(filename) diff --git a/examples/test/test_run_examples.py b/examples/test/test_run_examples.py new file mode 100644 index 000000000..210c0119c --- /dev/null +++ b/examples/test/test_run_examples.py @@ -0,0 +1,26 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test all examples.""" +from importlib import import_module + +import pytest + +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, monkeypatch): + module = import_module(name=import_path, package=__package__) + try: + run_function = module.run + except AttributeError: + pytest.skip("Module lacks 'run' function.") + return + + kwargs = build_kwargs(function=run_function, temp_dir=tmp_path) + + monkeypatch.setenv("AWS_DEFAULT_REGION", default_region()) + + run_function(**kwargs) diff --git a/requirements.txt b/requirements.txt index 7f8f0d532..08e1d1a72 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ +six boto3>=1.4.4 cryptography>=1.8.1 -attrs>=17.4.0 +attrs>=19.1.0 wrapt>=1.10.11 diff --git a/setup.cfg b/setup.cfg index 038fc5924..0671c3c64 100644 --- a/setup.cfg +++ b/setup.cfg @@ -52,4 +52,4 @@ force_grid_wrap = 0 combine_as_imports = True not_skip = __init__.py known_first_party = aws_encryption_sdk -known_third_party = attr,awses_test_vectors,basic_encryption,basic_file_encryption_with_multiple_providers,basic_file_encryption_with_raw_key_provider,boto3,botocore,cryptography,data_key_caching_basic,integration_test_utils,mock,pytest,pytest_mock,setuptools,six,typing,wrapt +known_third_party = attr,awacs,aws_encryption_sdk_decrypt_oracle,awses_test_vectors,boto3,botocore,chalice,cryptography,integration_test_utils,mock,moto,pytest,pytest_mock,requests,setuptools,six,troposphere,wrapt diff --git a/setup.py b/setup.py index 6ceb2d8fb..52ef4ff7f 100644 --- a/setup.py +++ b/setup.py @@ -48,10 +48,10 @@ def get_requirements(): "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Security", "Topic :: Security :: Cryptography", diff --git a/src/aws_encryption_sdk/__init__.py b/src/aws_encryption_sdk/__init__.py index 3f6d86e2e..b6a47537c 100644 --- a/src/aws_encryption_sdk/__init__.py +++ b/src/aws_encryption_sdk/__init__.py @@ -1,15 +1,5 @@ -# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). You -# may not use this file except in compliance with the License. A copy of -# the License is located at -# -# http://aws.amazon.com/apache2.0/ -# -# or in the "license" file accompanying this file. This file is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF -# ANY KIND, either express or implied. See the License for the specific -# language governing permissions and limitations under the License. +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 """High level AWS Encryption SDK client functions.""" # Below are imported for ease of use by implementors from aws_encryption_sdk.caches.local import LocalCryptoMaterialsCache # noqa @@ -24,6 +14,9 @@ StreamDecryptor, StreamEncryptor, ) +from aws_encryption_sdk.structures import CryptoResult + +__all__ = ("encrypt", "decrypt", "stream") def encrypt(**kwargs): @@ -33,28 +26,41 @@ def encrypt(**kwargs): When using this function, the entire ciphertext message is encrypted into memory before returning any data. If streaming is desired, see :class:`aws_encryption_sdk.stream`. + .. versionadded:: 1.5.0 + The *keyring* parameter. + + .. versionadded:: 1.5.0 + + For backwards compatibility, + the new :class:`CryptoResult` return value also unpacks like a 2-member tuple. + This allows for backwards compatibility with the previous outputs + so this change should not break any existing consumers. + .. code:: python >>> import aws_encryption_sdk - >>> kms_key_provider = aws_encryption_sdk.KMSMasterKeyProvider(key_ids=[ - ... 'arn:aws:kms:us-east-1:2222222222222:key/22222222-2222-2222-2222-222222222222', - ... 'arn:aws:kms:us-east-1:3333333333333:key/33333333-3333-3333-3333-333333333333' - ... ]) + >>> from aws_encryption_sdk.keyrings.aws_kms import KmsKeyring + >>> keyring = KmsKeyring( + ... generator_key_id="arn:aws:kms:us-east-1:2222222222222:key/22222222-2222-2222-2222-222222222222", + ... key_ids=["arn:aws:kms:us-east-1:3333333333333:key/33333333-3333-3333-3333-333333333333"], + ... ) >>> my_ciphertext, encryptor_header = aws_encryption_sdk.encrypt( ... source=my_plaintext, - ... key_provider=kms_key_provider - ... ) + ... keyring=keyring, + >>> ) :param config: Client configuration object (config or individual parameters required) :type config: aws_encryption_sdk.streaming_client.EncryptorConfig :param source: Source data to encrypt or decrypt :type source: str, bytes, io.IOBase, or file - :param materials_manager: `CryptoMaterialsManager` from which to obtain cryptographic materials - (either `materials_manager` or `key_provider` required) - :type materials_manager: aws_encryption_sdk.materials_managers.base.CryptoMaterialsManager - :param key_provider: `MasterKeyProvider` from which to obtain data keys for encryption - (either `materials_manager` or `key_provider` required) - :type key_provider: aws_encryption_sdk.key_providers.base.MasterKeyProvider + :param CryptoMaterialsManager materials_manager: + Cryptographic materials manager to use for encryption + (either ``materials_manager``, ``keyring``, ``key_provider`` required) + :param Keyring keyring: Keyring to use for encryption + (either ``materials_manager``, ``keyring``, ``key_provider`` required) + :param MasterKeyProvider key_provider: + Master key provider to use for encryption + (either ``materials_manager``, ``keyring``, ``key_provider`` required) :param int source_length: Length of source data (optional) .. note:: @@ -72,12 +78,13 @@ def encrypt(**kwargs): :param algorithm: Algorithm to use for encryption :type algorithm: aws_encryption_sdk.identifiers.Algorithm :param int frame_length: Frame length in bytes - :returns: Tuple containing the encrypted ciphertext and the message header object - :rtype: tuple of bytes and :class:`aws_encryption_sdk.structures.MessageHeader` + :returns: Encrypted message, message metadata (header), and keyring trace + :rtype: CryptoResult """ with StreamEncryptor(**kwargs) as encryptor: ciphertext = encryptor.read() - return ciphertext, encryptor.header + + return CryptoResult(result=ciphertext, header=encryptor.header, keyring_trace=encryptor.keyring_trace) def decrypt(**kwargs): @@ -87,28 +94,41 @@ def decrypt(**kwargs): When using this function, the entire ciphertext message is decrypted into memory before returning any data. If streaming is desired, see :class:`aws_encryption_sdk.stream`. + .. versionadded:: 1.5.0 + The *keyring* parameter. + + .. versionadded:: 1.5.0 + + For backwards compatibility, + the new :class:`CryptoResult` return value also unpacks like a 2-member tuple. + This allows for backwards compatibility with the previous outputs + so this change should not break any existing consumers. + .. code:: python >>> import aws_encryption_sdk - >>> kms_key_provider = aws_encryption_sdk.KMSMasterKeyProvider(key_ids=[ - ... 'arn:aws:kms:us-east-1:2222222222222:key/22222222-2222-2222-2222-222222222222', - ... 'arn:aws:kms:us-east-1:3333333333333:key/33333333-3333-3333-3333-333333333333' - ... ]) - >>> my_ciphertext, encryptor_header = aws_encryption_sdk.decrypt( + >>> from aws_encryption_sdk.keyrings.aws_kms import KmsKeyring + >>> keyring = KmsKeyring( + ... generator_key_id="arn:aws:kms:us-east-1:2222222222222:key/22222222-2222-2222-2222-222222222222", + ... key_ids=["arn:aws:kms:us-east-1:3333333333333:key/33333333-3333-3333-3333-333333333333"], + ... ) + >>> my_ciphertext, decryptor_header = aws_encryption_sdk.decrypt( ... source=my_ciphertext, - ... key_provider=kms_key_provider + ... keyring=keyring, ... ) :param config: Client configuration object (config or individual parameters required) :type config: aws_encryption_sdk.streaming_client.DecryptorConfig :param source: Source data to encrypt or decrypt :type source: str, bytes, io.IOBase, or file - :param materials_manager: `CryptoMaterialsManager` from which to obtain cryptographic materials - (either `materials_manager` or `key_provider` required) - :type materials_manager: aws_encryption_sdk.materials_managers.base.CryptoMaterialsManager - :param key_provider: `MasterKeyProvider` from which to obtain data keys for decryption - (either `materials_manager` or `key_provider` required) - :type key_provider: aws_encryption_sdk.key_providers.base.MasterKeyProvider + :param CryptoMaterialsManager materials_manager: + Cryptographic materials manager to use for encryption + (either ``materials_manager``, ``keyring``, ``key_provider`` required) + :param Keyring keyring: Keyring to use for encryption + (either ``materials_manager``, ``keyring``, ``key_provider`` required) + :param MasterKeyProvider key_provider: + Master key provider to use for encryption + (either ``materials_manager``, ``keyring``, ``key_provider`` required) :param int source_length: Length of source data (optional) .. note:: @@ -117,12 +137,13 @@ def decrypt(**kwargs): :param int max_body_length: Maximum frame size (or content length for non-framed messages) in bytes to read from ciphertext message. - :returns: Tuple containing the decrypted plaintext and the message header object - :rtype: tuple of bytes and :class:`aws_encryption_sdk.structures.MessageHeader` + :returns: Decrypted plaintext, message metadata (header), and keyring trace + :rtype: CryptoResult """ with StreamDecryptor(**kwargs) as decryptor: plaintext = decryptor.read() - return plaintext, decryptor.header + + return CryptoResult(result=plaintext, header=decryptor.header, keyring_trace=decryptor.keyring_trace) def stream(**kwargs): @@ -145,17 +166,18 @@ def stream(**kwargs): .. code:: python >>> import aws_encryption_sdk - >>> kms_key_provider = aws_encryption_sdk.KMSMasterKeyProvider(key_ids=[ - ... 'arn:aws:kms:us-east-1:2222222222222:key/22222222-2222-2222-2222-222222222222', - ... 'arn:aws:kms:us-east-1:3333333333333:key/33333333-3333-3333-3333-333333333333' - ... ]) + >>> from aws_encryption_sdk.keyrings.aws_kms import KmsKeyring + >>> keyring = KmsKeyring( + ... generator_key_id="arn:aws:kms:us-east-1:2222222222222:key/22222222-2222-2222-2222-222222222222", + ... key_ids=["arn:aws:kms:us-east-1:3333333333333:key/33333333-3333-3333-3333-333333333333"], + ... ) >>> plaintext_filename = 'my-secret-data.dat' >>> ciphertext_filename = 'my-encrypted-data.ct' >>> with open(plaintext_filename, 'rb') as pt_file, open(ciphertext_filename, 'wb') as ct_file: ... with aws_encryption_sdk.stream( ... mode='e', ... source=pt_file, - ... key_provider=kms_key_provider + ... keyring=keyring, ... ) as encryptor: ... for chunk in encryptor: ... ct_file.write(chunk) @@ -164,7 +186,7 @@ def stream(**kwargs): ... with aws_encryption_sdk.stream( ... mode='d', ... source=ct_file, - ... key_provider=kms_key_provider + ... keyring=keyring, ... ) as decryptor: ... for chunk in decryptor: ... pt_file.write(chunk) @@ -182,6 +204,3 @@ def stream(**kwargs): return _stream_map[mode.lower()](**kwargs) except KeyError: raise ValueError("Unsupported mode: {}".format(mode)) - - -__all__ = ("encrypt", "decrypt", "stream") diff --git a/src/aws_encryption_sdk/exceptions.py b/src/aws_encryption_sdk/exceptions.py index a71d414c0..3c58dcea1 100644 --- a/src/aws_encryption_sdk/exceptions.py +++ b/src/aws_encryption_sdk/exceptions.py @@ -1,15 +1,5 @@ -# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). You -# may not use this file except in compliance with the License. A copy of -# the License is located at -# -# http://aws.amazon.com/apache2.0/ -# -# or in the "license" file accompanying this file. This file is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF -# ANY KIND, either express or implied. See the License for the specific -# language governing permissions and limitations under the License. +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 """Contains exception classes for AWS Encryption SDK.""" @@ -53,6 +43,13 @@ class InvalidDataKeyError(AWSEncryptionSDKClientError): """Exception class for Invalid Data Keys.""" +class InvalidKeyringTraceError(AWSEncryptionSDKClientError): + """Exception class for invalid Keyring Traces. + + .. versionadded:: 1.5.0 + """ + + class InvalidProviderIdError(AWSEncryptionSDKClientError): """Exception class for Invalid Provider IDs.""" @@ -73,6 +70,20 @@ class DecryptKeyError(AWSEncryptionSDKClientError): """Exception class for errors encountered when MasterKeys try to decrypt data keys.""" +class SignatureKeyError(AWSEncryptionSDKClientError): + """Exception class for errors encountered with signing or verification keys. + + .. versionadded:: 1.5.0 + """ + + +class InvalidCryptographicMaterialsError(AWSEncryptionSDKClientError): + """Exception class for errors encountered when attempting to validate cryptographic materials. + + .. versionadded:: 1.5.0 + """ + + class ActionNotAllowedError(AWSEncryptionSDKClientError): """Exception class for errors encountered when attempting to perform unallowed actions.""" diff --git a/src/aws_encryption_sdk/identifiers.py b/src/aws_encryption_sdk/identifiers.py index e3c13c1ea..269afd702 100644 --- a/src/aws_encryption_sdk/identifiers.py +++ b/src/aws_encryption_sdk/identifiers.py @@ -14,6 +14,7 @@ import struct from enum import Enum +import attr from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import ec, padding, rsa from cryptography.hazmat.primitives.ciphers import algorithms, modes @@ -328,3 +329,27 @@ class ContentAADString(Enum): FRAME_STRING_ID = b"AWSKMSEncryptionClient Frame" FINAL_FRAME_STRING_ID = b"AWSKMSEncryptionClient Final Frame" NON_FRAMED_STRING_ID = b"AWSKMSEncryptionClient Single Block" + + +class KeyringTraceFlag(Enum): + """KeyRing Trace actions.""" + + @attr.s + class KeyringTraceFlagValue(object): + """Keyring trace flags do not have defined serializable values.""" + + name = attr.ib() + + #: A flag to represent that a keyring has generated a plaintext data key. + GENERATED_DATA_KEY = KeyringTraceFlagValue("GENERATED_DATA_KEY") + #: A flag to represent that a keyring has created an encrypted data key. + ENCRYPTED_DATA_KEY = KeyringTraceFlagValue("ENCRYPTED_DATA_KEY") + #: A flag to represent that a keyring has obtained + #: the corresponding plaintext data key from an encrypted data key. + DECRYPTED_DATA_KEY = KeyringTraceFlagValue("DECRYPTED_DATA_KEY") + #: A flag to represent that the keyring has cryptographically + #: bound the encryption context to a newly created encrypted data key. + SIGNED_ENCRYPTION_CONTEXT = KeyringTraceFlagValue("SIGNED_ENCRYPTION_CONTEXT") + #: A flag to represent that the keyring has verified that an encrypted + #: data key was originally created with a particular encryption context. + VERIFIED_ENCRYPTION_CONTEXT = KeyringTraceFlagValue("VERIFIED_ENCRYPTION_CONTEXT") diff --git a/src/aws_encryption_sdk/internal/formatting/serialize.py b/src/aws_encryption_sdk/internal/formatting/serialize.py index e7c86a0cb..bd71b3a1a 100644 --- a/src/aws_encryption_sdk/internal/formatting/serialize.py +++ b/src/aws_encryption_sdk/internal/formatting/serialize.py @@ -316,6 +316,6 @@ def serialize_wrapped_key(key_provider, wrapping_algorithm, wrapping_key_id, enc ) key_ciphertext = encrypted_wrapped_key.ciphertext + encrypted_wrapped_key.tag return EncryptedDataKey( - key_provider=MasterKeyInfo(provider_id=key_provider.provider_id, key_info=key_info), + key_provider=MasterKeyInfo(provider_id=key_provider.provider_id, key_info=key_info, key_name=wrapping_key_id), encrypted_data_key=key_ciphertext, ) diff --git a/src/aws_encryption_sdk/internal/validators.py b/src/aws_encryption_sdk/internal/validators.py new file mode 100644 index 000000000..66056602e --- /dev/null +++ b/src/aws_encryption_sdk/internal/validators.py @@ -0,0 +1,21 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Common ``attrs`` validators.""" +import attr # only used by mypy, so pylint: disable=unused-import +import six + +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Any # noqa pylint: disable=unused-import +except ImportError: # pragma: no cover + # We only actually need these imports when running the mypy checks + pass + + +def value_is_not_a_string(instance, attribute, value): + # type: (Any, attr.Attribute, Any) -> None + """Technically a string is an iterable containing strings. + + This validator lets you accept other iterators but not strings. + """ + if isinstance(value, six.string_types): + raise TypeError("'{}' must not a string".format(attribute.name)) diff --git a/src/aws_encryption_sdk/key_providers/base.py b/src/aws_encryption_sdk/key_providers/base.py index 3112cba6d..4ba10585b 100644 --- a/src/aws_encryption_sdk/key_providers/base.py +++ b/src/aws_encryption_sdk/key_providers/base.py @@ -13,6 +13,7 @@ """Base class interface for Master Key Providers.""" import abc import logging +import warnings import attr import six @@ -26,8 +27,17 @@ MasterKeyProviderError, ) from aws_encryption_sdk.internal.str_ops import to_bytes +from aws_encryption_sdk.structures import DataKey # pylint: disable=unused-import +from aws_encryption_sdk.structures import EncryptedDataKey # pylint: disable=unused-import +from aws_encryption_sdk.structures import RawDataKey # pylint: disable=unused-import from aws_encryption_sdk.structures import MasterKeyInfo +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Iterable, Union # noqa pylint: disable=unused-import +except ImportError: # pragma: no cover + # We only actually need these imports when running the mypy checks + pass + _LOGGER = logging.getLogger(__name__) @@ -42,6 +52,10 @@ class MasterKeyProviderConfig(object): class MasterKeyProvider(object): """Parent interface for Master Key Provider classes. + .. versionadded:: 1.5.0 + Master key providers are deprecated. + Use :class:`aws_encryption_sdk.keyrings.base.Keyring` instead. + :param config: Configuration object :type config: aws_encryption_sdk.key_providers.base.MasterKeyProviderConfig """ @@ -69,6 +83,16 @@ def __new__(cls, **kwargs): """Set key index and member set for all new instances here to avoid requiring child classes to call super init. """ + # DeprecationWarning are ignored by default, + # but because we are not yet removing master key providers, + # I think this is the correct level of visibility. + # + # Once we decide that we are one X or Y version away from removing master key providers, + # we should upgrade this to a UserWarning. + warnings.warn( + "Master key providers are deprecated as of 1.5.0. You should migrate to keyrings.", DeprecationWarning + ) + instance = super(MasterKeyProvider, cls).__new__(cls) config = kwargs.pop("config", None) if not isinstance(config, instance._config_class): # pylint: disable=protected-access @@ -212,6 +236,35 @@ def master_key_for_decrypt(self, key_info): self._decrypt_key_index[key_info] = decrypt_master_key return decrypt_master_key + def master_keys_for_data_key(self, data_key): + # type: (Union[DataKey, EncryptedDataKey, RawDataKey]) -> Iterable[MasterKey] + """Locates the correct master keys from children for the specified data key. + + :param data_key: Data key for which to locate owning master keys + :type data_key: :class:`EncryptedDataKey`, :class:`RawDataKey`, or :class:`DataKey` + :returns: Masters key that own data key + :rtype: iterator of :class:`MasterKey` + :raises UnknownIdentityError: if unable to locate the correct master key + """ + for member in [self] + self._members: + if member.provider_id != data_key.key_provider.provider_id: + continue + + _LOGGER.debug("attempting to locate master key from key provider: %s", member.provider_id) + + if isinstance(member, MasterKey): + if member.owns_data_key(data_key): + _LOGGER.debug("using existing master key") + yield member + + if self.vend_masterkey_on_decrypt: + try: + _LOGGER.debug("attempting to add master key: %s", data_key.key_provider.key_info) + yield member.master_key_for_decrypt(data_key.key_provider.key_info) + except InvalidKeyIdError: + _LOGGER.debug("master key %s not available in provider", data_key.key_provider.key_info) + continue + def decrypt_data_key(self, encrypted_data_key, algorithm, encryption_context): """Iterates through all currently added Master Keys and Master Key Providers to attempt to decrypt data key. @@ -225,42 +278,25 @@ def decrypt_data_key(self, encrypted_data_key, algorithm, encryption_context): :rtype: aws_encryption_sdk.structures.DataKey :raises DecryptKeyError: if unable to decrypt encrypted data key """ - data_key = None - master_key = None _LOGGER.debug("starting decrypt data key attempt") - for member in [self] + self._members: - if member.provider_id == encrypted_data_key.key_provider.provider_id: - _LOGGER.debug("attempting to locate master key from key provider: %s", member.provider_id) - if isinstance(member, MasterKey): - _LOGGER.debug("using existing master key") - master_key = member - elif self.vend_masterkey_on_decrypt: - try: - _LOGGER.debug("attempting to add master key: %s", encrypted_data_key.key_provider.key_info) - master_key = member.master_key_for_decrypt(encrypted_data_key.key_provider.key_info) - except InvalidKeyIdError: - _LOGGER.debug( - "master key %s not available in provider", encrypted_data_key.key_provider.key_info - ) - continue - else: - continue - try: - _LOGGER.debug( - "attempting to decrypt data key with provider %s", encrypted_data_key.key_provider.key_info - ) - data_key = master_key.decrypt_data_key(encrypted_data_key, algorithm, encryption_context) - except (IncorrectMasterKeyError, DecryptKeyError) as error: - _LOGGER.debug( - "%s raised when attempting to decrypt data key with master key %s", - repr(error), - master_key.key_provider, - ) - continue - break # If this point is reached without throwing any errors, the data key has been decrypted - if not data_key: - raise DecryptKeyError("Unable to decrypt data key") - return data_key + for master_key in self.master_keys_for_data_key(encrypted_data_key): + try: + _LOGGER.debug( + "attempting to decrypt data key with provider %s", encrypted_data_key.key_provider.key_info + ) + return master_key.decrypt_data_key(encrypted_data_key, algorithm, encryption_context) + + # MasterKeyProvider.decrypt_data_key throws DecryptKeyError + # but MasterKey.decrypt_data_key throws IncorrectMasterKeyError + except (IncorrectMasterKeyError, DecryptKeyError) as error: + _LOGGER.debug( + "%s raised when attempting to decrypt data key with master key %s", + repr(error), + master_key.key_provider, + ) + continue + + raise DecryptKeyError("Unable to decrypt data key") def decrypt_data_key_from_list(self, encrypted_data_keys, algorithm, encryption_context): """Receives a list of encrypted data keys and returns the first one which this provider is able to decrypt. @@ -308,6 +344,10 @@ def __attrs_post_init__(self): class MasterKey(MasterKeyProvider): """Parent interface for Master Key classes. + .. versionadded:: 1.5.0 + Master key providers are deprecated. + Use :class:`aws_encryption_sdk.keyrings.base.Keyring` instead. + :param bytes key_id: Key ID for Master Key :param config: Configuration object :type config: aws_encryption_sdk.key_providers.base.MasterKeyConfig @@ -356,9 +396,7 @@ def owns_data_key(self, data_key): :returns: Boolean statement of ownership :rtype: bool """ - if data_key.key_provider == self.key_provider: - return True - return False + return data_key.key_provider == self.key_provider def master_keys_for_encryption(self, encryption_context, plaintext_rostream, plaintext_length=None): """Returns self and a list containing self, to match the format of output for a Master Key Provider. diff --git a/src/aws_encryption_sdk/key_providers/kms.py b/src/aws_encryption_sdk/key_providers/kms.py index c0a2dc46e..08398b568 100644 --- a/src/aws_encryption_sdk/key_providers/kms.py +++ b/src/aws_encryption_sdk/key_providers/kms.py @@ -78,6 +78,10 @@ class KMSMasterKeyProviderConfig(MasterKeyProviderConfig): class KMSMasterKeyProvider(MasterKeyProvider): """Master Key Provider for KMS. + .. versionadded:: 1.5.0 + Master key providers are deprecated. + Use :class:`aws_encryption_sdk.keyrings.aws_kms.KmsKeyring` instead. + >>> import aws_encryption_sdk >>> kms_key_provider = aws_encryption_sdk.KMSMasterKeyProvider(key_ids=[ ... 'arn:aws:kms:us-east-1:2222222222222:key/22222222-2222-2222-2222-222222222222', @@ -226,6 +230,10 @@ def client_default(self): class KMSMasterKey(MasterKey): """Master Key class for KMS CMKs. + .. versionadded:: 1.5.0 + Master key providers are deprecated. + Use :class:`aws_encryption_sdk.keyrings.aws_kms.KmsKeyring` instead. + :param config: Configuration object (config or individual parameters required) :type config: aws_encryption_sdk.key_providers.kms.KMSMasterKeyConfig :param bytes key_id: KMS CMK ID diff --git a/src/aws_encryption_sdk/key_providers/raw.py b/src/aws_encryption_sdk/key_providers/raw.py index 57a1d5edf..fff3487e2 100644 --- a/src/aws_encryption_sdk/key_providers/raw.py +++ b/src/aws_encryption_sdk/key_providers/raw.py @@ -49,6 +49,11 @@ class RawMasterKeyConfig(MasterKeyConfig): class RawMasterKey(MasterKey): """Raw Master Key. + .. versionadded:: 1.5.0 + Master key providers are deprecated. + Use :class:`aws_encryption_sdk.keyrings.raw.RawAESKeyring` + or :class:`aws_encryption_sdk.keyrings.raw.RawRSAKeyring` instead. + :param config: Configuration object (config or individual parameters required) :type config: aws_encryption_sdk.key_providers.raw.RawMasterKeyConfig :param bytes key_id: Key ID for Master Key @@ -192,6 +197,11 @@ def _decrypt_data_key(self, encrypted_data_key, algorithm, encryption_context): class RawMasterKeyProvider(MasterKeyProvider): """Raw Master Key Provider. + .. versionadded:: 1.5.0 + Master key providers are deprecated. + Use :class:`aws_encryption_sdk.keyrings.raw.RawAESKeyring` + or :class:`aws_encryption_sdk.keyrings.raw.RawRSAKeyring` instead. + :param config: Configuration object (optional) :type config: aws_encryption_sdk.key_providers.base.MasterKeyProviderConfig """ diff --git a/examples/test/test_i_data_key_caching_basic.py b/src/aws_encryption_sdk/keyrings/__init__.py similarity index 50% rename from examples/test/test_i_data_key_caching_basic.py rename to src/aws_encryption_sdk/keyrings/__init__.py index 734c35692..ada03b4d7 100644 --- a/examples/test/test_i_data_key_caching_basic.py +++ b/src/aws_encryption_sdk/keyrings/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2017-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"). You # may not use this file except in compliance with the License. A copy of @@ -10,16 +10,4 @@ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. -"""Unit test suite for the basic data key caching example in the AWS-hosted documentation.""" -import pytest - -from ..src.data_key_caching_basic import encrypt_with_caching -from .examples_test_utils import get_cmk_arn - - -pytestmark = [pytest.mark.examples] - - -def test_encrypt_with_caching(): - cmk_arn = get_cmk_arn() - encrypt_with_caching(kms_cmk_arn=cmk_arn, max_age_in_cache=10.0, cache_capacity=10) +"""All provided Keyrings.""" diff --git a/src/aws_encryption_sdk/keyrings/aws_kms/__init__.py b/src/aws_encryption_sdk/keyrings/aws_kms/__init__.py new file mode 100644 index 000000000..678ac722b --- /dev/null +++ b/src/aws_encryption_sdk/keyrings/aws_kms/__init__.py @@ -0,0 +1,392 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Keyring for use with AWS Key Management Service (KMS). + +.. versionadded:: 1.5.0 + +""" +import logging + +import attr +import six +from attr.validators import deep_iterable, instance_of, is_callable, optional + +from aws_encryption_sdk.exceptions import DecryptKeyError, EncryptKeyError +from aws_encryption_sdk.identifiers import AlgorithmSuite +from aws_encryption_sdk.internal.validators import value_is_not_a_string +from aws_encryption_sdk.keyrings.base import Keyring +from aws_encryption_sdk.keyrings.multi import MultiKeyring +from aws_encryption_sdk.materials_managers import DecryptionMaterials, EncryptionMaterials +from aws_encryption_sdk.structures import EncryptedDataKey, KeyringTrace, KeyringTraceFlag, MasterKeyInfo, RawDataKey + +from .client_suppliers import DefaultClientSupplier + +from .client_suppliers import ClientSupplier # noqa - only used in docstring params; this confuses flake8 + +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Dict, Iterable, Union # noqa pylint: disable=unused-import + from .client_suppliers import ClientSupplierType # noqa pylint: disable=unused-import +except ImportError: # pragma: no cover + # We only actually need these imports when running the mypy checks + pass + +__all__ = ("KmsKeyring", "KEY_NAMESPACE") + +_LOGGER = logging.getLogger(__name__) +_GENERATE_FLAGS = {KeyringTraceFlag.GENERATED_DATA_KEY} +_ENCRYPT_FLAGS = {KeyringTraceFlag.ENCRYPTED_DATA_KEY, KeyringTraceFlag.SIGNED_ENCRYPTION_CONTEXT} +_DECRYPT_FLAGS = {KeyringTraceFlag.DECRYPTED_DATA_KEY, KeyringTraceFlag.VERIFIED_ENCRYPTION_CONTEXT} + +#: Key namespace used for all encrypted data keys created by the KMS keyring. +KEY_NAMESPACE = "aws-kms" + + +@attr.s +class KmsKeyring(Keyring): + """Keyring that uses AWS Key Management Service (KMS) Customer Master Keys (CMKs) to manage wrapping keys. + + Set ``generator_key_id`` to require that the keyring use that CMK to generate the data key. + If you do not set ``generator_key_id``, the keyring will not generate a data key. + + Set ``key_ids`` to specify additional CMKs that the keyring will use to encrypt the data key. + + The keyring will attempt to use any CMKs + identified by CMK ARN in either ``generator_key_id`` or ``key_ids`` on decrypt. + + You can identify CMKs by any `valid key ID`_ for the keyring to use on encrypt, + but for the keyring to attempt to use them on decrypt + you MUST specify the CMK ARN. + + If you specify ``is_discovery=True`` the keyring will be a KMS discovery keyring, + doing nothing on encrypt and attempting to decrypt any AWS KMS-encrypted data key on decrypt. + + .. note:: + + You must either set ``is_discovery=True`` or provide key IDs. + + You can use the :class:`ClientSupplier` to customize behavior further, + such as to provide different credentials for different regions + or to restrict which regions are allowed. + + See the `AWS KMS Keyring specification`_ for more details. + + .. _AWS KMS Keyring specification: + https://github.com/awslabs/aws-encryption-sdk-specification/blob/master/framework/kms-keyring.md + .. _valid key ID: + https://docs.aws.amazon.com/kms/latest/APIReference/API_GenerateDataKey.html#API_GenerateDataKey_RequestSyntax + .. _discovery mode: + https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#kms-keyring-discovery + + .. versionadded:: 1.5.0 + + :param ClientSupplier client_supplier: Client supplier that provides AWS KMS clients (optional) + :param bool is_discovery: Should this be a discovery keyring (optional) + :param str generator_key_id: Key ID of AWS KMS CMK to use when generating data keys (optional) + :param List[str] key_ids: Key IDs that will be used to encrypt and decrypt data keys (optional) + :param List[str] grant_tokens: AWS KMS grant tokens to include in requests (optional) + """ + + _client_supplier = attr.ib(default=attr.Factory(DefaultClientSupplier), validator=is_callable()) + _is_discovery = attr.ib(default=False, validator=instance_of(bool)) + _generator_key_id = attr.ib(default=None, validator=optional(instance_of(six.string_types))) + _key_ids = attr.ib( + default=attr.Factory(tuple), + validator=(deep_iterable(member_validator=instance_of(six.string_types)), value_is_not_a_string), + ) + _grant_tokens = attr.ib( + default=attr.Factory(tuple), + validator=(deep_iterable(member_validator=instance_of(six.string_types)), value_is_not_a_string), + ) + + def __attrs_post_init__(self): + """Configure internal keyring.""" + key_ids_provided = self._generator_key_id is not None or self._key_ids + both = key_ids_provided and self._is_discovery + neither = not key_ids_provided and not self._is_discovery + + if both: + raise TypeError("is_discovery cannot be True if key IDs are provided") + + if neither: + raise TypeError("is_discovery cannot be False if no key IDs are provided") + + if self._is_discovery: + self._inner_keyring = _AwsKmsDiscoveryKeyring( + client_supplier=self._client_supplier, grant_tokens=self._grant_tokens + ) + return + + if self._generator_key_id is None: + generator_keyring = None + else: + generator_keyring = _AwsKmsSingleCmkKeyring( + key_id=self._generator_key_id, client_supplier=self._client_supplier, grant_tokens=self._grant_tokens + ) + + child_keyrings = [ + _AwsKmsSingleCmkKeyring( + key_id=key_id, client_supplier=self._client_supplier, grant_tokens=self._grant_tokens + ) + for key_id in self._key_ids + ] + + self._inner_keyring = MultiKeyring(generator=generator_keyring, children=child_keyrings) + + def on_encrypt(self, encryption_materials): + # type: (EncryptionMaterials) -> EncryptionMaterials + """Generate a data key using generator keyring + and encrypt it using any available wrapping key in any child keyring. + + :param EncryptionMaterials encryption_materials: Encryption materials for keyring to modify. + :returns: Optionally modified encryption materials. + :rtype: EncryptionMaterials + :raises EncryptKeyError: if unable to encrypt data key. + """ + return self._inner_keyring.on_encrypt(encryption_materials=encryption_materials) + + def on_decrypt(self, decryption_materials, encrypted_data_keys): + # type: (DecryptionMaterials, Iterable[EncryptedDataKey]) -> DecryptionMaterials + """Attempt to decrypt the encrypted data keys. + + :param DecryptionMaterials decryption_materials: Decryption materials for keyring to modify. + :param List[EncryptedDataKey] encrypted_data_keys: List of encrypted data keys. + :returns: Optionally modified decryption materials. + :rtype: DecryptionMaterials + """ + return self._inner_keyring.on_decrypt( + decryption_materials=decryption_materials, encrypted_data_keys=encrypted_data_keys + ) + + +@attr.s +class _AwsKmsSingleCmkKeyring(Keyring): + """AWS KMS keyring that only works with a single AWS KMS CMK. + + This keyring should never be used directly. + It should only ever be used internally by :class:`KmsKeyring`. + + .. versionadded:: 1.5.0 + + :param str key_id: CMK key ID + :param ClientSupplier client_supplier: Client supplier to use when asking for clients + :param List[str] grant_tokens: AWS KMS grant tokens to include in requests (optional) + """ + + _key_id = attr.ib(validator=instance_of(six.string_types)) + _client_supplier = attr.ib(validator=is_callable()) + _grant_tokens = attr.ib( + default=attr.Factory(tuple), + validator=(deep_iterable(member_validator=instance_of(six.string_types)), value_is_not_a_string), + ) + + def on_encrypt(self, encryption_materials): + # type: (EncryptionMaterials) -> EncryptionMaterials + trace_info = MasterKeyInfo(provider_id=KEY_NAMESPACE, key_info=self._key_id) + new_materials = encryption_materials + try: + if new_materials.data_encryption_key is None: + plaintext_key, encrypted_key = _do_aws_kms_generate_data_key( + client_supplier=self._client_supplier, + key_name=self._key_id, + encryption_context=new_materials.encryption_context, + algorithm=new_materials.algorithm, + grant_tokens=self._grant_tokens, + ) + new_materials = new_materials.with_data_encryption_key( + data_encryption_key=plaintext_key, + keyring_trace=KeyringTrace(wrapping_key=trace_info, flags=_GENERATE_FLAGS), + ) + else: + encrypted_key = _do_aws_kms_encrypt( + client_supplier=self._client_supplier, + key_name=self._key_id, + plaintext_data_key=new_materials.data_encryption_key, + encryption_context=new_materials.encryption_context, + grant_tokens=self._grant_tokens, + ) + except Exception: # pylint: disable=broad-except + # We intentionally WANT to catch all exceptions here + message = "Unable to generate or encrypt data key using {}".format(trace_info) + _LOGGER.exception(message) + raise EncryptKeyError(message) + + return new_materials.with_encrypted_data_key( + encrypted_data_key=encrypted_key, keyring_trace=KeyringTrace(wrapping_key=trace_info, flags=_ENCRYPT_FLAGS) + ) + + def on_decrypt(self, decryption_materials, encrypted_data_keys): + # type: (DecryptionMaterials, Iterable[EncryptedDataKey]) -> DecryptionMaterials + new_materials = decryption_materials + + for edk in encrypted_data_keys: + if new_materials.data_encryption_key is not None: + return new_materials + + if ( + edk.key_provider.provider_id == KEY_NAMESPACE + and edk.key_provider.key_info.decode("utf-8") == self._key_id + ): + new_materials = _try_aws_kms_decrypt( + client_supplier=self._client_supplier, + decryption_materials=new_materials, + grant_tokens=self._grant_tokens, + encrypted_data_key=edk, + ) + + return new_materials + + +@attr.s +class _AwsKmsDiscoveryKeyring(Keyring): + """AWS KMS discovery keyring that will attempt to decrypt any AWS KMS encrypted data key. + + This keyring should never be used directly. + It should only ever be used internally by :class:`KmsKeyring`. + + .. versionadded:: 1.5.0 + + :param ClientSupplier client_supplier: Client supplier to use when asking for clients + :param List[str] grant_tokens: AWS KMS grant tokens to include in requests (optional) + """ + + _client_supplier = attr.ib(validator=is_callable()) + _grant_tokens = attr.ib( + default=attr.Factory(tuple), + validator=(deep_iterable(member_validator=instance_of(six.string_types)), value_is_not_a_string), + ) + + def on_encrypt(self, encryption_materials): + # type: (EncryptionMaterials) -> EncryptionMaterials + return encryption_materials + + def on_decrypt(self, decryption_materials, encrypted_data_keys): + # type: (DecryptionMaterials, Iterable[EncryptedDataKey]) -> DecryptionMaterials + new_materials = decryption_materials + + for edk in encrypted_data_keys: + if new_materials.data_encryption_key is not None: + return new_materials + + if edk.key_provider.provider_id == KEY_NAMESPACE: + new_materials = _try_aws_kms_decrypt( + client_supplier=self._client_supplier, + decryption_materials=new_materials, + grant_tokens=self._grant_tokens, + encrypted_data_key=edk, + ) + + return new_materials + + +def _try_aws_kms_decrypt(client_supplier, decryption_materials, grant_tokens, encrypted_data_key): + # type: (ClientSupplierType, DecryptionMaterials, Iterable[str], EncryptedDataKey) -> DecryptionMaterials + """Attempt to call ``kms:Decrypt`` and return the resulting plaintext data key. + + Any errors encountered are caught and logged. + + .. versionadded:: 1.5.0 + + """ + try: + plaintext_key = _do_aws_kms_decrypt( + client_supplier=client_supplier, + key_name=encrypted_data_key.key_provider.key_info.decode("utf-8"), + encrypted_data_key=encrypted_data_key, + encryption_context=decryption_materials.encryption_context, + grant_tokens=grant_tokens, + ) + except Exception: # pylint: disable=broad-except + # We intentionally WANT to catch all exceptions here + _LOGGER.exception("Unable to decrypt encrypted data key from %s", encrypted_data_key.key_provider) + return decryption_materials + + return decryption_materials.with_data_encryption_key( + data_encryption_key=plaintext_key, + keyring_trace=KeyringTrace(wrapping_key=encrypted_data_key.key_provider, flags=_DECRYPT_FLAGS), + ) + + +def _do_aws_kms_decrypt(client_supplier, key_name, encrypted_data_key, encryption_context, grant_tokens): + # type: (ClientSupplierType, str, EncryptedDataKey, Dict[str, str], Iterable[str]) -> RawDataKey + """Attempt to call ``kms:Decrypt`` and return the resulting plaintext data key. + + Any errors encountered are passed up the chain without comment. + + .. versionadded:: 1.5.0 + + """ + region = _region_from_key_id(encrypted_data_key.key_provider.key_info.decode("utf-8")) + client = client_supplier(region) + response = client.decrypt( + CiphertextBlob=encrypted_data_key.encrypted_data_key, + EncryptionContext=encryption_context, + GrantTokens=grant_tokens, + ) + response_key_id = response["KeyId"] + if response_key_id != key_name: + raise DecryptKeyError( + "Decryption results from AWS KMS are for an unexpected key ID!" + " actual '{actual}' != expected '{expected}'".format(actual=response_key_id, expected=key_name) + ) + return RawDataKey( + key_provider=MasterKeyInfo(provider_id=KEY_NAMESPACE, key_info=response_key_id), data_key=response["Plaintext"] + ) + + +def _do_aws_kms_encrypt(client_supplier, key_name, plaintext_data_key, encryption_context, grant_tokens): + # type: (ClientSupplierType, str, RawDataKey, Dict[str, str], Iterable[str]) -> EncryptedDataKey + """Attempt to call ``kms:Encrypt`` and return the resulting encrypted data key. + + Any errors encountered are passed up the chain without comment. + """ + region = _region_from_key_id(key_name) + client = client_supplier(region) + response = client.encrypt( + KeyId=key_name, + Plaintext=plaintext_data_key.data_key, + EncryptionContext=encryption_context, + GrantTokens=grant_tokens, + ) + return EncryptedDataKey( + key_provider=MasterKeyInfo(provider_id=KEY_NAMESPACE, key_info=response["KeyId"]), + encrypted_data_key=response["CiphertextBlob"], + ) + + +def _do_aws_kms_generate_data_key(client_supplier, key_name, encryption_context, algorithm, grant_tokens): + # type: (ClientSupplierType, str, Dict[str, str], AlgorithmSuite, Iterable[str]) -> (RawDataKey, EncryptedDataKey) + """Attempt to call ``kms:GenerateDataKey`` and return the resulting plaintext and encrypted data keys. + + Any errors encountered are passed up the chain without comment. + + .. versionadded:: 1.5.0 + + """ + region = _region_from_key_id(key_name) + client = client_supplier(region) + response = client.generate_data_key( + KeyId=key_name, + NumberOfBytes=algorithm.kdf_input_len, + EncryptionContext=encryption_context, + GrantTokens=grant_tokens, + ) + provider = MasterKeyInfo(provider_id=KEY_NAMESPACE, key_info=response["KeyId"]) + plaintext_key = RawDataKey(key_provider=provider, data_key=response["Plaintext"]) + encrypted_key = EncryptedDataKey(key_provider=provider, encrypted_data_key=response["CiphertextBlob"]) + return plaintext_key, encrypted_key + + +def _region_from_key_id(key_id): + # type: (str) -> Union[None, str] + """Attempt to determine the region from the key ID. + + If the region cannot be found, ``None`` is returned instead. + + .. versionadded:: 1.5.0 + + """ + parts = key_id.split(":", 4) + try: + return parts[3] + except IndexError: + return None diff --git a/src/aws_encryption_sdk/keyrings/aws_kms/_client_cache.py b/src/aws_encryption_sdk/keyrings/aws_kms/_client_cache.py new file mode 100644 index 000000000..9eb8d3e82 --- /dev/null +++ b/src/aws_encryption_sdk/keyrings/aws_kms/_client_cache.py @@ -0,0 +1,119 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""boto3 client cache for use by client suppliers. + +.. versionadded:: 1.5.0 + +.. warning:: + No guarantee is provided on the modules and APIs within this + namespace staying consistent. Directly reference at your own risk. + +""" +import functools +import logging + +import attr +from attr.validators import instance_of +from boto3.session import Session as Boto3Session +from botocore.client import BaseClient +from botocore.config import Config as BotocoreConfig +from botocore.exceptions import BotoCoreError +from botocore.session import Session as BotocoreSession + +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Dict # noqa pylint: disable=unused-import +except ImportError: # pragma: no cover + # We only actually need these imports when running the mypy checks + pass + +_LOGGER = logging.getLogger(__name__) +__all__ = ("ClientCache",) + + +@attr.s +class ClientCache(object): + """Provide boto3 clients regional clients, caching by region. + + Any clients that throw an error when used are immediately removed from the cache. + + .. versionadded:: 1.5.0 + + :param botocore_session: Botocore session to use when creating clients + :type botocore_session: botocore.session.Session + :param client_config: Config to use when creating client + :type client_config: botocore.config.Config + """ + + _botocore_session = attr.ib(validator=instance_of(BotocoreSession)) + _client_config = attr.ib(validator=instance_of(BotocoreConfig)) + + def __attrs_post_init__(self): + """Set up internal cache.""" + self._cache = {} # type: Dict[str, BaseClient] + + def _wrap_client_method(self, region_name, method, *args, **kwargs): + """Proxy a call to a boto3 client method and remove any misbehaving clients from the cache. + + :param str region_name: Client region name + :param Callable method: Method on the boto3 client to proxy + :param Tuple args: Positional arguments to pass to ``method`` + :param Dict kwargs: Named arguments to pass to ``method`` + :returns: result of + """ + try: + return method(*args, **kwargs) + except BotoCoreError as error: + try: + del self._cache[region_name] + except KeyError: + pass + _LOGGER.exception( + 'Removing client "%s" from cache due to BotoCoreError on %s call', region_name, method.__name__ + ) + raise error + + def _patch_client(self, client): + # type: (BaseClient) -> BaseClient + """Patch a boto3 client, wrapping every API call in ``_wrap_client_method``. + + :param BaseClient client: boto3 client to patch + :returns: patched client + """ + for method_name in client.meta.method_to_api_mapping: + method = getattr(client, method_name) + wrapped_method = functools.partial(self._wrap_client_method, client.meta.region_name, method) + setattr(client, method_name, wrapped_method) + + return client + + def _add_client(self, region_name, service): + # type: (str, str) -> BaseClient + """Make a new client and add it to the internal cache. + + :param str region_name: Client region + :param str service: Client service + :returns: New client, now in cache + :rtype: botocore.client.BaseClient + """ + client = Boto3Session(botocore_session=self._botocore_session).client( + service_name=service, region_name=region_name, config=self._client_config + ) + patched_client = self._patch_client(client) + self._cache[region_name] = patched_client + return client + + def client(self, region_name, service): + # type: (str, str) -> BaseClient + """Get a client for the specified region and service. + + Generate a new client if needed. + Otherwise, retrieve an existing client from the internal cache. + + :param str region_name: Client region + :param str service: Client service + :rtype: botocore.client.BaseClient + """ + try: + return self._cache[region_name] + except KeyError: + return self._add_client(region_name, service) diff --git a/src/aws_encryption_sdk/keyrings/aws_kms/client_suppliers.py b/src/aws_encryption_sdk/keyrings/aws_kms/client_suppliers.py new file mode 100644 index 000000000..c8a0af696 --- /dev/null +++ b/src/aws_encryption_sdk/keyrings/aws_kms/client_suppliers.py @@ -0,0 +1,159 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""AWS KMS client suppliers for use with AWS KMS keyring. + +.. versionadded:: 1.5.0 + +""" +import functools +import logging + +import attr +import six +from attr.validators import deep_iterable, instance_of, is_callable, optional +from botocore.client import BaseClient +from botocore.config import Config as BotocoreConfig +from botocore.session import Session as BotocoreSession + +from aws_encryption_sdk.exceptions import UnknownRegionError +from aws_encryption_sdk.identifiers import USER_AGENT_SUFFIX +from aws_encryption_sdk.internal.validators import value_is_not_a_string + +from ._client_cache import ClientCache + +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Callable, Union # noqa pylint: disable=unused-import + + ClientSupplierType = Callable[[Union[None, str]], BaseClient] +except ImportError: # pragma: no cover + # We only actually need these imports when running the mypy checks + pass + +_LOGGER = logging.getLogger(__name__) +__all__ = ( + "ClientSupplier", + "ClientSupplierType", + "DefaultClientSupplier", + "AllowRegionsClientSupplier", + "DenyRegionsClientSupplier", +) + + +class ClientSupplier(object): + """Base class for client suppliers. + + .. versionadded:: 1.5.0 + + """ + + def __call__(self, region_name): + # type: (Union[None, str]) -> BaseClient + """Return a client for the requested region. + + :rtype: BaseClient + """ + raise NotImplementedError("'ClientSupplier' is not callable") + + +@attr.s +class DefaultClientSupplier(ClientSupplier): + """The default AWS KMS client supplier. + Creates and caches clients for any region. + + .. versionadded:: 1.5.0 + + If you want clients to have special credentials or other configuration, + you can provide those with custom ``botocore`` Session and/or `Config`_ instances. + + .. _Config: https://botocore.amazonaws.com/v1/documentation/api/latest/reference/config.html + + .. code-block:: python + + from aws_encryption_sdk.keyrings.aws_kms.client_supplier import DefaultClientSupplier + from botocore.session import Session + from botocore.config import Config + + my_client_supplier = DefaultClientSupplier( + botocore_session=Session(**_get_custom_credentials()), + client_config=Config(connect_timeout=10), + ) + + :param botocore_session: Botocore session to use when creating clients (optional) + :type botocore_session: botocore.session.Session + :param client_config: Config to use when creating client (optional) + :type client_config: botocore.config.Config + """ + + _botocore_session = attr.ib(default=attr.Factory(BotocoreSession), validator=instance_of(BotocoreSession)) + _client_config = attr.ib( + default=attr.Factory(functools.partial(BotocoreConfig, user_agent_extra=USER_AGENT_SUFFIX)), + validator=instance_of(BotocoreConfig), + ) + + def __attrs_post_init__(self): + """Set up the internal cache.""" + self._client_cache = ClientCache(botocore_session=self._botocore_session, client_config=self._client_config) + + def __call__(self, region_name): + # type: (Union[None, str]) -> BaseClient + """Return a client for the requested region. + + :rtype: BaseClient + """ + return self._client_cache.client(region_name=region_name, service="kms") + + +@attr.s +class AllowRegionsClientSupplier(ClientSupplier): + """AWS KMS client supplier that only supplies clients for the specified regions. + + .. versionadded:: 1.5.0 + + :param List[str] allowed_regions: Regions to allow + :param ClientSupplier client_supplier: Client supplier to wrap (optional) + """ + + allowed_regions = attr.ib( + validator=(deep_iterable(member_validator=instance_of(six.string_types)), value_is_not_a_string) + ) + _client_supplier = attr.ib(default=attr.Factory(DefaultClientSupplier), validator=optional(is_callable())) + + def __call__(self, region_name): + # type: (Union[None, str]) -> BaseClient + """Return a client for the requested region. + + :rtype: BaseClient + :raises UnknownRegionError: if a region is requested that is not in ``allowed_regions`` + """ + if region_name not in self.allowed_regions: + raise UnknownRegionError("Unable to provide client for region '{}'".format(region_name)) + + return self._client_supplier(region_name) + + +@attr.s +class DenyRegionsClientSupplier(ClientSupplier): + """AWS KMS client supplier that supplies clients for any region except for the specified regions. + + .. versionadded:: 1.5.0 + + :param List[str] denied_regions: Regions to deny + :param ClientSupplier client_supplier: Client supplier to wrap (optional) + """ + + denied_regions = attr.ib( + validator=(deep_iterable(member_validator=instance_of(six.string_types)), value_is_not_a_string) + ) + _client_supplier = attr.ib(default=attr.Factory(DefaultClientSupplier), validator=optional(is_callable())) + + def __call__(self, region_name): + # type: (Union[None, str]) -> BaseClient + """Return a client for the requested region. + + :rtype: BaseClient + :raises UnknownRegionError: if a region is requested that is in ``denied_regions`` + """ + if region_name in self.denied_regions: + raise UnknownRegionError("Unable to provide client for region '{}'".format(region_name)) + + return self._client_supplier(region_name) diff --git a/src/aws_encryption_sdk/keyrings/base.py b/src/aws_encryption_sdk/keyrings/base.py new file mode 100644 index 000000000..c854faf27 --- /dev/null +++ b/src/aws_encryption_sdk/keyrings/base.py @@ -0,0 +1,46 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Base class interface for Keyrings.""" +from aws_encryption_sdk.materials_managers import ( # only used for mypy; pylint: disable=unused-import + DecryptionMaterials, + EncryptionMaterials, +) +from aws_encryption_sdk.structures import EncryptedDataKey # only used for mypy; pylint: disable=unused-import + +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Iterable # noqa pylint: disable=unused-import +except ImportError: # pragma: no cover + # We only actually need these imports when running the mypy checks + pass + +__all__ = ("Keyring",) + + +class Keyring(object): + """Parent interface for Keyring classes. + + .. versionadded:: 1.5.0 + """ + + def on_encrypt(self, encryption_materials): + # type: (EncryptionMaterials) -> EncryptionMaterials + """Generate a data key if not present and encrypt it using any available wrapping key. + + :param EncryptionMaterials encryption_materials: Encryption materials for keyring to modify. + :returns: Optionally modified encryption materials. + :rtype: EncryptionMaterials + :raises NotImplementedError: if method is not implemented + """ + raise NotImplementedError("Keyring does not implement on_encrypt function") + + def on_decrypt(self, decryption_materials, encrypted_data_keys): + # type: (DecryptionMaterials, Iterable[EncryptedDataKey]) -> DecryptionMaterials + """Attempt to decrypt the encrypted data keys. + + :param DecryptionMaterials decryption_materials: Decryption materials for keyring to modify. + :param List[EncryptedDataKey] encrypted_data_keys: List of encrypted data keys. + :returns: Optionally modified decryption materials. + :rtype: DecryptionMaterials + :raises NotImplementedError: if method is not implemented + """ + raise NotImplementedError("Keyring does not implement on_decrypt function") diff --git a/src/aws_encryption_sdk/keyrings/multi.py b/src/aws_encryption_sdk/keyrings/multi.py new file mode 100644 index 000000000..27e90c3c8 --- /dev/null +++ b/src/aws_encryption_sdk/keyrings/multi.py @@ -0,0 +1,105 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Resources required for Multi Keyrings.""" +import itertools + +import attr +from attr.validators import deep_iterable, instance_of, optional + +from aws_encryption_sdk.exceptions import EncryptKeyError, GenerateKeyError +from aws_encryption_sdk.keyrings.base import Keyring + +from aws_encryption_sdk.materials_managers import ( # only used for mypy; pylint: disable=unused-import + DecryptionMaterials, + EncryptionMaterials, +) +from aws_encryption_sdk.structures import EncryptedDataKey # only used for mypy; pylint: disable=unused-import + +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Iterable # noqa pylint: disable=unused-import +except ImportError: # pragma: no cover + # We only actually need these imports when running the mypy checks + pass + +__all__ = ("MultiKeyring",) + + +@attr.s +class MultiKeyring(Keyring): + """Public class for Multi Keyring. + + .. versionadded:: 1.5.0 + + :param Keyring generator: Generator keyring used to generate data encryption key (optional) + :param List[Keyring] children: List of keyrings used to encrypt the data encryption key (optional) + :raises EncryptKeyError: if encryption of data key fails for any reason + """ + + generator = attr.ib(default=None, validator=optional(instance_of(Keyring))) + children = attr.ib( + default=attr.Factory(tuple), validator=optional(deep_iterable(member_validator=instance_of(Keyring))) + ) + + def __attrs_post_init__(self): + # type: () -> None + """Prepares initial values not handled by attrs.""" + neither_generator_nor_children = self.generator is None and not self.children + if neither_generator_nor_children: + raise TypeError("At least one of generator or children must be provided") + + _generator = (self.generator,) if self.generator is not None else () + self._decryption_keyrings = list(itertools.chain(_generator, self.children)) + + def on_encrypt(self, encryption_materials): + # type: (EncryptionMaterials) -> EncryptionMaterials + """Generate a data key using generator keyring + and encrypt it using any available wrapping key in any child keyring. + + :param EncryptionMaterials encryption_materials: Encryption materials for keyring to modify. + :returns: Optionally modified encryption materials. + :rtype: EncryptionMaterials + :raises EncryptKeyError: if unable to encrypt data key. + """ + # Check if generator keyring is not provided and data key is not generated + if self.generator is None and encryption_materials.data_encryption_key is None: + raise EncryptKeyError( + "Generator keyring not provided " + "and encryption materials do not already contain a plaintext data key." + ) + + new_materials = encryption_materials + + # Call on_encrypt on the generator keyring if it is provided + if self.generator is not None: + new_materials = self.generator.on_encrypt(encryption_materials=new_materials) + + # Check if data key is generated + if new_materials.data_encryption_key is None: + raise GenerateKeyError("Unable to generate data encryption key.") + + # Call on_encrypt on all other keyrings + for keyring in self.children: + new_materials = keyring.on_encrypt(encryption_materials=new_materials) + + return new_materials + + def on_decrypt(self, decryption_materials, encrypted_data_keys): + # type: (DecryptionMaterials, Iterable[EncryptedDataKey]) -> DecryptionMaterials + """Attempt to decrypt the encrypted data keys. + + :param DecryptionMaterials decryption_materials: Decryption materials for keyring to modify. + :param List[EncryptedDataKey] encrypted_data_keys: List of encrypted data keys. + :returns: Optionally modified decryption materials. + :rtype: DecryptionMaterials + """ + # Call on_decrypt on all keyrings till decryption is successful + new_materials = decryption_materials + for keyring in self._decryption_keyrings: + if new_materials.data_encryption_key is not None: + return new_materials + + new_materials = keyring.on_decrypt( + decryption_materials=new_materials, encrypted_data_keys=encrypted_data_keys + ) + + return new_materials diff --git a/src/aws_encryption_sdk/keyrings/raw.py b/src/aws_encryption_sdk/keyrings/raw.py new file mode 100644 index 000000000..450111ad7 --- /dev/null +++ b/src/aws_encryption_sdk/keyrings/raw.py @@ -0,0 +1,461 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Resources required for Raw Keyrings.""" +import logging +import os + +import attr +import six +from attr.validators import in_, instance_of, optional +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey, RSAPublicKey + +from aws_encryption_sdk.exceptions import EncryptKeyError, GenerateKeyError +from aws_encryption_sdk.identifiers import EncryptionKeyType, KeyringTraceFlag, WrappingAlgorithm +from aws_encryption_sdk.internal.crypto.wrapping_keys import EncryptedData, WrappingKey +from aws_encryption_sdk.internal.formatting.deserialize import deserialize_wrapped_key +from aws_encryption_sdk.internal.formatting.serialize import serialize_raw_master_key_prefix, serialize_wrapped_key +from aws_encryption_sdk.key_providers.raw import RawMasterKey +from aws_encryption_sdk.keyrings.base import Keyring +from aws_encryption_sdk.materials_managers import DecryptionMaterials, EncryptionMaterials +from aws_encryption_sdk.structures import EncryptedDataKey, KeyringTrace, MasterKeyInfo, RawDataKey + +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Iterable # noqa pylint: disable=unused-import +except ImportError: # pragma: no cover + # We only actually need these imports when running the mypy checks + pass + +__all__ = ("RawAESKeyring", "RawRSAKeyring") +_LOGGER = logging.getLogger(__name__) + + +def _generate_data_key( + encryption_materials, # type: EncryptionMaterials + key_provider, # type: MasterKeyInfo +): + # type: (...) -> EncryptionMaterials + """Generates plaintext data key for the keyring. + + :param EncryptionMaterials encryption_materials: Encryption materials for the keyring to modify. + :param MasterKeyInfo key_provider: Information about the key in the keyring. + :rtype: EncryptionMaterials + :returns: Encryption materials containing a data encryption key + """ + # Check if encryption materials contain data encryption key + if encryption_materials.data_encryption_key is not None: + raise TypeError("Data encryption key already exists.") + + # Generate data key + try: + plaintext_data_key = os.urandom(encryption_materials.algorithm.kdf_input_len) + except Exception: # pylint: disable=broad-except + error_message = "Unable to generate data encryption key." + _LOGGER.exception(error_message) + raise GenerateKeyError("Unable to generate data encryption key.") + + # Create a keyring trace + keyring_trace = KeyringTrace(wrapping_key=key_provider, flags={KeyringTraceFlag.GENERATED_DATA_KEY}) + + # plaintext_data_key to RawDataKey + data_encryption_key = RawDataKey(key_provider=key_provider, data_key=plaintext_data_key) + + return encryption_materials.with_data_encryption_key( + data_encryption_key=data_encryption_key, keyring_trace=keyring_trace + ) + + +@attr.s +class RawAESKeyring(Keyring): + """Generate an instance of Raw AES Keyring which encrypts using AES-GCM algorithm using wrapping key provided as a + byte array + + .. versionadded:: 1.5.0 + + :param str key_namespace: String defining the keyring. + :param bytes key_name: Key ID + :param bytes wrapping_key: Encryption key with which to wrap plaintext data key. + + .. note:: + + Only one wrapping key can be specified in a Raw AES Keyring + """ + + key_namespace = attr.ib(validator=instance_of(six.string_types)) + key_name = attr.ib(validator=instance_of(six.binary_type)) + _wrapping_key = attr.ib(repr=False, validator=instance_of(six.binary_type)) + + def __attrs_post_init__(self): + # type: () -> None + """Prepares initial values not handled by attrs.""" + key_size_to_wrapping_algorithm = { + wrapper.algorithm.kdf_input_len: wrapper + for wrapper in ( + WrappingAlgorithm.AES_128_GCM_IV12_TAG16_NO_PADDING, + WrappingAlgorithm.AES_192_GCM_IV12_TAG16_NO_PADDING, + WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING, + ) + } + + try: + self._wrapping_algorithm = key_size_to_wrapping_algorithm[len(self._wrapping_key)] + except KeyError: + raise ValueError( + "Invalid wrapping key length. Must be one of {} bytes.".format( + sorted(key_size_to_wrapping_algorithm.keys()) + ) + ) + + self._key_provider = MasterKeyInfo(provider_id=self.key_namespace, key_info=self.key_name) + + self._wrapping_key_structure = WrappingKey( + wrapping_algorithm=self._wrapping_algorithm, + wrapping_key=self._wrapping_key, + wrapping_key_type=EncryptionKeyType.SYMMETRIC, + ) + + self._key_info_prefix = self._get_key_info_prefix( + key_namespace=self.key_namespace, key_name=self.key_name, wrapping_key=self._wrapping_key_structure + ) + + @staticmethod + def _get_key_info_prefix(key_namespace, key_name, wrapping_key): + # type: (str, bytes, WrappingKey) -> six.binary_type + """Helper function to get key info prefix + + :param str key_namespace: String defining the keyring. + :param bytes key_name: Key ID + :param WrappingKey wrapping_key: Encryption key with which to wrap plaintext data key. + :return: Serialized key_info prefix + :rtype: bytes + """ + key_info_prefix = serialize_raw_master_key_prefix( + RawMasterKey(provider_id=key_namespace, key_id=key_name, wrapping_key=wrapping_key) + ) + return key_info_prefix + + def on_encrypt(self, encryption_materials): + # type: (EncryptionMaterials) -> EncryptionMaterials + """Generate a data key if not present and encrypt it using any available wrapping key + + :param EncryptionMaterials encryption_materials: Encryption materials for the keyring to modify + :returns: Encryption materials containing data key and encrypted data key + :rtype: EncryptionMaterials + """ + new_materials = encryption_materials + + if new_materials.data_encryption_key is None: + # Get encryption materials with a new data key. + new_materials = _generate_data_key(encryption_materials=new_materials, key_provider=self._key_provider) + + try: + # Encrypt data key + encrypted_wrapped_key = self._wrapping_key_structure.encrypt( + plaintext_data_key=new_materials.data_encryption_key.data_key, + encryption_context=new_materials.encryption_context, + ) + + # EncryptedData to EncryptedDataKey + encrypted_data_key = serialize_wrapped_key( + key_provider=self._key_provider, + wrapping_algorithm=self._wrapping_algorithm, + wrapping_key_id=self.key_name, + encrypted_wrapped_key=encrypted_wrapped_key, + ) + except Exception: # pylint: disable=broad-except + error_message = "Raw AES keyring unable to encrypt data key" + _LOGGER.exception(error_message) + raise EncryptKeyError(error_message) + + # Update Keyring Trace + keyring_trace = KeyringTrace( + wrapping_key=self._key_provider, + flags={KeyringTraceFlag.ENCRYPTED_DATA_KEY, KeyringTraceFlag.SIGNED_ENCRYPTION_CONTEXT}, + ) + + return new_materials.with_encrypted_data_key(encrypted_data_key=encrypted_data_key, keyring_trace=keyring_trace) + + def on_decrypt(self, decryption_materials, encrypted_data_keys): + # type: (DecryptionMaterials, Iterable[EncryptedDataKey]) -> DecryptionMaterials + """Attempt to decrypt the encrypted data keys. + + :param DecryptionMaterials decryption_materials: Decryption materials for the keyring to modify + :param List[EncryptedDataKey] encrypted_data_keys: List of encrypted data keys + :returns: Decryption materials that MAY include a plaintext data key + :rtype: DecryptionMaterials + """ + new_materials = decryption_materials + + if new_materials.data_encryption_key is not None: + return new_materials + + # Decrypt data key + expected_key_info_len = len(self._key_info_prefix) + self._wrapping_algorithm.algorithm.iv_len + for key in encrypted_data_keys: + + if ( + key.key_provider.provider_id != self._key_provider.provider_id + or len(key.key_provider.key_info) != expected_key_info_len + or not key.key_provider.key_info.startswith(self._key_info_prefix) + ): + continue + + # Wrapped EncryptedDataKey to deserialized EncryptedData + encrypted_wrapped_key = deserialize_wrapped_key( + wrapping_algorithm=self._wrapping_algorithm, wrapping_key_id=self.key_name, wrapped_encrypted_key=key + ) + + # EncryptedData to raw key string + try: + plaintext_data_key = self._wrapping_key_structure.decrypt( + encrypted_wrapped_data_key=encrypted_wrapped_key, + encryption_context=new_materials.encryption_context, + ) + + except Exception: # pylint: disable=broad-except + # We intentionally WANT to catch all exceptions here + error_message = "Raw AES Keyring unable to decrypt data key" + _LOGGER.exception(error_message) + # The Raw AES keyring MUST evaluate every encrypted data key + # until it either succeeds or runs out of encrypted data keys. + continue + + # Create a keyring trace + keyring_trace = KeyringTrace( + wrapping_key=self._key_provider, + flags={KeyringTraceFlag.DECRYPTED_DATA_KEY, KeyringTraceFlag.VERIFIED_ENCRYPTION_CONTEXT}, + ) + + # Update decryption materials + data_encryption_key = RawDataKey(key_provider=self._key_provider, data_key=plaintext_data_key) + + return new_materials.with_data_encryption_key( + data_encryption_key=data_encryption_key, keyring_trace=keyring_trace + ) + + return new_materials + + +@attr.s +class RawRSAKeyring(Keyring): + """Generate an instance of Raw RSA Keyring which performs asymmetric encryption and decryption using public + and private keys provided + + .. versionadded:: 1.5.0 + + :param str key_namespace: String defining the keyring ID + :param bytes key_name: Key ID + :param cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey private_wrapping_key: + Private encryption key with which to wrap plaintext data key (optional) + :param cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey public_wrapping_key: + Public encryption key with which to wrap plaintext data key (optional) + :param WrappingAlgorithm wrapping_algorithm: Wrapping Algorithm with which to wrap plaintext data key + :param MasterKeyInfo key_provider: Complete information about the key in the keyring + + .. note:: + + At least one of public wrapping key or private wrapping key must be provided. + """ + + key_namespace = attr.ib(validator=instance_of(six.string_types)) + key_name = attr.ib(validator=instance_of(six.binary_type)) + _wrapping_algorithm = attr.ib( + repr=False, + validator=in_( + ( + WrappingAlgorithm.RSA_PKCS1, + WrappingAlgorithm.RSA_OAEP_SHA1_MGF1, + WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, + WrappingAlgorithm.RSA_OAEP_SHA384_MGF1, + WrappingAlgorithm.RSA_OAEP_SHA512_MGF1, + ) + ), + ) + _private_wrapping_key = attr.ib(default=None, repr=False, validator=optional(instance_of(RSAPrivateKey))) + _public_wrapping_key = attr.ib(default=None, repr=False, validator=optional(instance_of(RSAPublicKey))) + + def __attrs_post_init__(self): + # type: () -> None + """Prepares initial values not handled by attrs.""" + self._key_provider = MasterKeyInfo(provider_id=self.key_namespace, key_info=self.key_name) + + if self._public_wrapping_key is None and self._private_wrapping_key is None: + raise TypeError("At least one of public key or private key must be provided.") + + if self._private_wrapping_key is not None and self._public_wrapping_key is None: + self._public_wrapping_key = self._private_wrapping_key.public_key() + + @classmethod + def from_pem_encoding( + cls, + key_namespace, # type: str + key_name, # type: bytes + wrapping_algorithm, # type: WrappingAlgorithm + public_encoded_key=None, # type: bytes + private_encoded_key=None, # type: bytes + password=None, # type: bytes + ): + # type: (...) -> RawRSAKeyring + """Generate a Raw RSA keyring using PEM Encoded public and private keys + + :param str key_namespace: String defining the keyring ID + :param bytes key_name: Key ID + :param WrappingAlgorithm wrapping_algorithm: Wrapping Algorithm with which to wrap plaintext data key + :param bytes public_encoded_key: PEM encoded public key (optional) + :param bytes private_encoded_key: PEM encoded private key (optional) + :param bytes password: Password to load private key (optional) + :return: :class:`RawRSAKeyring` constructed using required parameters + """ + loaded_private_wrapping_key = loaded_public_wrapping_key = None + if private_encoded_key is not None: + loaded_private_wrapping_key = serialization.load_pem_private_key( + data=private_encoded_key, password=password, backend=default_backend() + ) + if public_encoded_key is not None: + loaded_public_wrapping_key = serialization.load_pem_public_key( + data=public_encoded_key, backend=default_backend() + ) + + return cls( + key_namespace=key_namespace, + key_name=key_name, + wrapping_algorithm=wrapping_algorithm, + private_wrapping_key=loaded_private_wrapping_key, + public_wrapping_key=loaded_public_wrapping_key, + ) + + @classmethod + def from_der_encoding( + cls, + key_namespace, # type: str + key_name, # type: bytes + wrapping_algorithm, # type: WrappingAlgorithm + public_encoded_key=None, # type: bytes + 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 + :param bytes key_name: Key ID + :param WrappingAlgorithm wrapping_algorithm: Wrapping Algorithm with which to wrap plaintext data key + :param bytes public_encoded_key: DER encoded public key (optional) + :param bytes private_encoded_key: DER encoded private key (optional) + :param bytes password: Password to load private key (optional) + :return: :class:`RawRSAKeyring` constructed using required parameters + """ + loaded_private_wrapping_key = loaded_public_wrapping_key = None + if private_encoded_key is not None: + loaded_private_wrapping_key = serialization.load_der_private_key( + data=private_encoded_key, password=password, backend=default_backend() + ) + if public_encoded_key is not None: + loaded_public_wrapping_key = serialization.load_der_public_key( + data=public_encoded_key, backend=default_backend() + ) + + return cls( + key_namespace=key_namespace, + key_name=key_name, + wrapping_algorithm=wrapping_algorithm, + private_wrapping_key=loaded_private_wrapping_key, + public_wrapping_key=loaded_public_wrapping_key, + ) + + def on_encrypt(self, encryption_materials): + # type: (EncryptionMaterials) -> EncryptionMaterials + """Generate a data key using generator keyring + and encrypt it using any available wrapping key in any child keyring. + + :param EncryptionMaterials encryption_materials: Encryption materials for keyring to modify. + :returns: Encryption materials containing data key and encrypted data key + :rtype: EncryptionMaterials + """ + new_materials = encryption_materials + + if new_materials.data_encryption_key is None: + new_materials = _generate_data_key(encryption_materials=new_materials, key_provider=self._key_provider) + + if self._public_wrapping_key is None: + # This should be impossible, but just in case, give a useful error message. + raise EncryptKeyError("Raw RSA keyring unable to encrypt data key: no public key available") + + try: + # Encrypt data key + encrypted_wrapped_key = EncryptedData( + iv=None, + ciphertext=self._public_wrapping_key.encrypt( + plaintext=new_materials.data_encryption_key.data_key, padding=self._wrapping_algorithm.padding, + ), + tag=None, + ) + + # EncryptedData to EncryptedDataKey + encrypted_data_key = serialize_wrapped_key( + key_provider=self._key_provider, + wrapping_algorithm=self._wrapping_algorithm, + wrapping_key_id=self.key_name, + encrypted_wrapped_key=encrypted_wrapped_key, + ) + except Exception: # pylint: disable=broad-except + error_message = "Raw RSA keyring unable to encrypt data key" + _LOGGER.exception(error_message) + raise EncryptKeyError(error_message) + + # Update Keyring Trace + keyring_trace = KeyringTrace(wrapping_key=self._key_provider, flags={KeyringTraceFlag.ENCRYPTED_DATA_KEY}) + + # Add encrypted data key to encryption_materials + return new_materials.with_encrypted_data_key(encrypted_data_key=encrypted_data_key, keyring_trace=keyring_trace) + + def on_decrypt(self, decryption_materials, encrypted_data_keys): + # type: (DecryptionMaterials, Iterable[EncryptedDataKey]) -> DecryptionMaterials + """Attempt to decrypt the encrypted data keys. + + :param DecryptionMaterials decryption_materials: Decryption materials for keyring to modify. + :param encrypted_data_keys: List of encrypted data keys. + :type: List[EncryptedDataKey] + :returns: Decryption materials that MAY include a plaintext data key + :rtype: DecryptionMaterials + """ + new_materials = decryption_materials + + if new_materials.data_encryption_key is not None: + return new_materials + + if self._private_wrapping_key is None: + return new_materials + + # Decrypt data key + for key in encrypted_data_keys: + if key.key_provider != self._key_provider: + continue + + # Wrapped EncryptedDataKey to deserialized EncryptedData + encrypted_wrapped_key = deserialize_wrapped_key( + wrapping_algorithm=self._wrapping_algorithm, wrapping_key_id=self.key_name, wrapped_encrypted_key=key + ) + try: + plaintext_data_key = self._private_wrapping_key.decrypt( + ciphertext=encrypted_wrapped_key.ciphertext, padding=self._wrapping_algorithm.padding + ) + except Exception: # pylint: disable=broad-except + error_message = "Raw RSA Keyring unable to decrypt data key" + _LOGGER.exception(error_message) + # The Raw RSA keyring MUST evaluate every encrypted data key + # until it either succeeds or runs out of encrypted data keys. + continue + + # Create a keyring trace + keyring_trace = KeyringTrace(wrapping_key=self._key_provider, flags={KeyringTraceFlag.DECRYPTED_DATA_KEY}) + + # Update decryption materials + data_encryption_key = RawDataKey(key_provider=self._key_provider, data_key=plaintext_data_key) + + return new_materials.with_data_encryption_key( + data_encryption_key=data_encryption_key, keyring_trace=keyring_trace + ) + + return new_materials diff --git a/src/aws_encryption_sdk/materials_managers/__init__.py b/src/aws_encryption_sdk/materials_managers/__init__.py index bc5230c51..8c8c33886 100644 --- a/src/aws_encryption_sdk/materials_managers/__init__.py +++ b/src/aws_encryption_sdk/materials_managers/__init__.py @@ -14,12 +14,23 @@ .. versionadded:: 1.3.0 """ +import copy + import attr import six +from attr.validators import deep_iterable, deep_mapping, instance_of, optional + +from aws_encryption_sdk.exceptions import InvalidDataKeyError, InvalidKeyringTraceError, SignatureKeyError +from aws_encryption_sdk.identifiers import Algorithm, KeyringTraceFlag +from aws_encryption_sdk.internal.crypto.authentication import Signer, Verifier +from aws_encryption_sdk.internal.utils.streams import ROStream +from aws_encryption_sdk.structures import DataKey, EncryptedDataKey, KeyringTrace, RawDataKey -from ..identifiers import Algorithm -from ..internal.utils.streams import ROStream -from ..structures import DataKey +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Any, Iterable, Tuple, Union # noqa pylint: disable=unused-import +except ImportError: # pragma: no cover + # We only actually need these imports when running the mypy checks + pass @attr.s(hash=False) @@ -40,38 +51,324 @@ class EncryptionMaterialsRequest(object): :param int plaintext_length: Length of source plaintext (optional) """ - encryption_context = attr.ib(validator=attr.validators.instance_of(dict)) - frame_length = attr.ib(validator=attr.validators.instance_of(six.integer_types)) - plaintext_rostream = attr.ib( - default=None, validator=attr.validators.optional(attr.validators.instance_of(ROStream)) + encryption_context = attr.ib( + validator=deep_mapping( + key_validator=instance_of(six.string_types), value_validator=instance_of(six.string_types) + ) + ) + frame_length = attr.ib(validator=instance_of(six.integer_types)) + plaintext_rostream = attr.ib(default=None, validator=optional(instance_of(ROStream))) + algorithm = attr.ib(default=None, validator=optional(instance_of(Algorithm))) + plaintext_length = attr.ib(default=None, validator=optional(instance_of(six.integer_types))) + + +def _data_key_to_raw_data_key(data_key): + # type: (Union[DataKey, RawDataKey, None]) -> Union[RawDataKey, None] + """Convert a :class:`DataKey` into a :class:`RawDataKey`.""" + if isinstance(data_key, RawDataKey) or data_key is None: + return data_key + + return RawDataKey.from_data_key(data_key=data_key) + + +@attr.s +class CryptographicMaterials(object): + """Cryptographic materials core. + + .. versionadded:: 1.5.0 + + :param Algorithm algorithm: Algorithm to use for encrypting message + :param dict encryption_context: Encryption context tied to `encrypted_data_keys` + :param RawDataKey data_encryption_key: Plaintext data key to use for encrypting message + :param keyring_trace: Any KeyRing trace entries + :type keyring_trace: list of :class:`KeyringTrace` + """ + + algorithm = attr.ib(validator=optional(instance_of(Algorithm))) + encryption_context = attr.ib( + validator=optional( + deep_mapping(key_validator=instance_of(six.string_types), value_validator=instance_of(six.string_types)) + ) ) - algorithm = attr.ib(default=None, validator=attr.validators.optional(attr.validators.instance_of(Algorithm))) - plaintext_length = attr.ib( - default=None, validator=attr.validators.optional(attr.validators.instance_of(six.integer_types)) + data_encryption_key = attr.ib( + default=None, validator=optional(instance_of(RawDataKey)), converter=_data_key_to_raw_data_key ) + _keyring_trace = attr.ib( + default=attr.Factory(list), validator=optional(deep_iterable(member_validator=instance_of(KeyringTrace))) + ) + _initialized = False + def __attrs_post_init__(self): + """Freeze attributes after initialization.""" + self._initialized = True -@attr.s(hash=False) -class EncryptionMaterials(object): + def __setattr__(self, key, value): + # type: (str, Any) -> None + """Do not allow attributes to be changed once an instance is initialized.""" + if self._initialized: + raise AttributeError("can't set attribute") + + self._setattr(key, value) + + def _setattr(self, key, value): + # type: (str, Any) -> None + """Special __setattr__ to avoid having to perform multi-level super calls.""" + super(CryptographicMaterials, self).__setattr__(key, value) + + def _validate_data_encryption_key(self, data_encryption_key, keyring_trace, required_flags): + # type: (Union[DataKey, RawDataKey], KeyringTrace, Iterable[KeyringTraceFlag]) -> None + """Validate that the provided data encryption key and keyring trace match for each other and the materials. + + .. versionadded:: 1.5.0 + + :param RawDataKey data_encryption_key: Data encryption key + :param KeyringTrace keyring_trace: Keyring trace corresponding to data_encryption_key + :param required_flags: Iterable of required flags + :type required_flags: iterable of :class:`KeyringTraceFlag` + :raises AttributeError: if data encryption key is already set + :raises InvalidKeyringTraceError: if keyring trace does not match decrypt action + :raises InvalidKeyringTraceError: if keyring trace does not match data key provider + :raises InvalidDataKeyError: if data key length does not match algorithm suite + """ + if self.data_encryption_key is not None: + raise AttributeError("Data encryption key is already set.") + + for flag in required_flags: + if flag not in keyring_trace.flags: + raise InvalidKeyringTraceError("Keyring flags do not match action.") + + if keyring_trace.wrapping_key != data_encryption_key.key_provider: + raise InvalidKeyringTraceError("Keyring trace does not match data key provider.") + + if len(data_encryption_key.data_key) != self.algorithm.kdf_input_len: + raise InvalidDataKeyError( + "Invalid data key length {actual} must be {expected}.".format( + actual=len(data_encryption_key.data_key), expected=self.algorithm.kdf_input_len + ) + ) + + def _with_data_encryption_key(self, data_encryption_key, keyring_trace, required_flags): + # type: (Union[DataKey, RawDataKey], KeyringTrace, Iterable[KeyringTraceFlag]) -> CryptographicMaterials + """Get new cryptographic materials that include this data encryption key. + + .. versionadded:: 1.5.0 + + :param RawDataKey data_encryption_key: Data encryption key + :param KeyringTrace keyring_trace: Trace of actions that a keyring performed + while getting this data encryption key + :param required_flags: Iterable of required flags + :type required_flags: iterable of :class:`KeyringTraceFlag` + :raises AttributeError: if data encryption key is already set + :raises InvalidKeyringTraceError: if keyring trace does not match required actions + :raises InvalidKeyringTraceError: if keyring trace does not match data key provider + :raises InvalidDataKeyError: if data key length does not match algorithm suite + """ + self._validate_data_encryption_key( + data_encryption_key=data_encryption_key, keyring_trace=keyring_trace, required_flags=required_flags + ) + + new_materials = copy.copy(self) + + data_key = _data_key_to_raw_data_key(data_key=data_encryption_key) + new_materials._setattr( # simplify access to copies pylint: disable=protected-access + "data_encryption_key", data_key + ) + new_materials._keyring_trace.append(keyring_trace) # simplify access to copies pylint: disable=protected-access + + return new_materials + + @property + def keyring_trace(self): + # type: () -> Tuple[KeyringTrace] + """Return a read-only version of the keyring trace. + + :rtype: tuple + """ + return tuple(self._keyring_trace) + + +@attr.s(hash=False, init=False) +class EncryptionMaterials(CryptographicMaterials): """Encryption materials returned by a crypto material manager's `get_encryption_materials` method. .. versionadded:: 1.3.0 - :param algorithm: Algorithm to use for encrypting message - :type algorithm: aws_encryption_sdk.identifiers.Algorithm - :param data_encryption_key: Plaintext data key to use for encrypting message - :type data_encryption_key: aws_encryption_sdk.structures.DataKey - :param encrypted_data_keys: List of encrypted data keys - :type encrypted_data_keys: list of `aws_encryption_sdk.structures.EncryptedDataKey` + .. versionadded:: 1.5.0 + + The **keyring_trace** parameter. + + .. versionadded:: 1.5.0 + + Most parameters are now optional. + + :param Algorithm algorithm: Algorithm to use for encrypting message + :param RawDataKey data_encryption_key: Plaintext data key to use for encrypting message (optional) + :param encrypted_data_keys: List of encrypted data keys (optional) + :type encrypted_data_keys: list of :class:`EncryptedDataKey` :param dict encryption_context: Encryption context tied to `encrypted_data_keys` - :param bytes signing_key: Encoded signing key + :param bytes signing_key: Encoded signing key (optional) + :param keyring_trace: Any KeyRing trace entries (optional) + :type keyring_trace: list of :class:`KeyringTrace` """ - algorithm = attr.ib(validator=attr.validators.instance_of(Algorithm)) - data_encryption_key = attr.ib(validator=attr.validators.instance_of(DataKey)) - encrypted_data_keys = attr.ib(validator=attr.validators.instance_of(set)) - encryption_context = attr.ib(validator=attr.validators.instance_of(dict)) - signing_key = attr.ib(default=None, validator=attr.validators.optional(attr.validators.instance_of(bytes))) + _encrypted_data_keys = attr.ib( + default=attr.Factory(list), validator=optional(deep_iterable(member_validator=instance_of(EncryptedDataKey))) + ) + signing_key = attr.ib(default=None, repr=False, validator=optional(instance_of(bytes))) + + def __init__( + self, + algorithm=None, + data_encryption_key=None, + encrypted_data_keys=None, + encryption_context=None, + signing_key=None, + **kwargs + ): # noqa we define this in the class docstring + if algorithm is None: + raise TypeError("algorithm must not be None") + + if encryption_context is None: + raise TypeError("encryption_context must not be None") + + if data_encryption_key is None and encrypted_data_keys: + # If data_encryption_key is not set, encrypted_data_keys MUST be either None or empty + raise TypeError("encrypted_data_keys cannot be provided without data_encryption_key") + + if encrypted_data_keys is None: + encrypted_data_keys = [] + + super(EncryptionMaterials, self).__init__( + algorithm=algorithm, + encryption_context=encryption_context, + data_encryption_key=data_encryption_key, + **kwargs + ) + self._setattr("signing_key", signing_key) + self._setattr("_encrypted_data_keys", encrypted_data_keys) + attr.validate(self) + + def __copy__(self): + # type: () -> EncryptionMaterials + """Do a shallow copy of this instance.""" + return EncryptionMaterials( + algorithm=self.algorithm, + data_encryption_key=self.data_encryption_key, + encrypted_data_keys=copy.copy(self._encrypted_data_keys), + encryption_context=self.encryption_context.copy(), + signing_key=self.signing_key, + keyring_trace=copy.copy(self._keyring_trace), + ) + + @property + def encrypted_data_keys(self): + # type: () -> Tuple[EncryptedDataKey] + """Return a read-only version of the encrypted data keys. + + :rtype: Tuple[EncryptedDataKey] + """ + return tuple(self._encrypted_data_keys) + + @property + def is_complete(self): + # type: () -> bool + """Determine whether these materials are sufficiently complete for use as encryption materials. + + :rtype: bool + """ + if self.data_encryption_key is None: + return False + + if not self.encrypted_data_keys: + return False + + if self.algorithm.signing_algorithm_info is not None and self.signing_key is None: + return False + + return True + + def with_data_encryption_key(self, data_encryption_key, keyring_trace): + # type: (Union[DataKey, RawDataKey], KeyringTrace) -> EncryptionMaterials + """Get new encryption materials that also include this data encryption key. + + .. versionadded:: 1.5.0 + + :param RawDataKey data_encryption_key: Data encryption key + :param KeyringTrace keyring_trace: Trace of actions that a keyring performed + while getting this data encryption key + :rtype: EncryptionMaterials + :raises AttributeError: if data encryption key is already set + :raises InvalidKeyringTraceError: if keyring trace does not match generate action + :raises InvalidKeyringTraceError: if keyring trace does not match data key provider + :raises InvalidDataKeyError: if data key length does not match algorithm suite + """ + return self._with_data_encryption_key( + data_encryption_key=data_encryption_key, + keyring_trace=keyring_trace, + required_flags={KeyringTraceFlag.GENERATED_DATA_KEY}, + ) + + def with_encrypted_data_key(self, encrypted_data_key, keyring_trace): + # type: (EncryptedDataKey, KeyringTrace) -> EncryptionMaterials + """Get new encryption materials that also include this encrypted data key with corresponding keyring trace. + + .. versionadded:: 1.5.0 + + :param EncryptedDataKey encrypted_data_key: Encrypted data key to add + :param KeyringTrace keyring_trace: Trace of actions that a keyring performed + while getting this encrypted data key + :rtype: EncryptionMaterials + :raises AttributeError: if data encryption key is not set + :raises InvalidKeyringTraceError: if keyring trace does not match generate action + :raises InvalidKeyringTraceError: if keyring trace does not match data key encryptor + """ + if self.data_encryption_key is None: + raise AttributeError("Data encryption key is not set.") + + if KeyringTraceFlag.ENCRYPTED_DATA_KEY not in keyring_trace.flags: + raise InvalidKeyringTraceError("Keyring flags do not match action.") + + if not all( + ( + keyring_trace.wrapping_key.provider_id == encrypted_data_key.key_provider.provider_id, + keyring_trace.wrapping_key.key_name == encrypted_data_key.key_provider.key_name, + ) + ): + raise InvalidKeyringTraceError("Keyring trace does not match data key encryptor.") + + new_materials = copy.copy(self) + + new_materials._encrypted_data_keys.append( # simplify access to copies pylint: disable=protected-access + encrypted_data_key + ) + new_materials._keyring_trace.append(keyring_trace) # simplify access to copies pylint: disable=protected-access + return new_materials + + def with_signing_key(self, signing_key): + # type: (bytes) -> EncryptionMaterials + """Get new encryption materials that also include this signing key. + + .. versionadded:: 1.5.0 + + :param bytes signing_key: Signing key + :rtype: EncryptionMaterials + :raises AttributeError: if signing key is already set + :raises SignatureKeyError: if algorithm suite does not support signing keys + """ + if self.signing_key is not None: + raise AttributeError("Signing key is already set.") + + if self.algorithm.signing_algorithm_info is None: + raise SignatureKeyError("Algorithm suite does not support signing keys.") + + new_materials = copy.copy(self) + + # Verify that the signing key matches the algorithm + Signer.from_key_bytes(algorithm=new_materials.algorithm, key_bytes=signing_key) + + new_materials._setattr("signing_key", signing_key) # simplify access to copies pylint: disable=protected-access + + return new_materials @attr.s(hash=False) @@ -87,21 +384,145 @@ class DecryptionMaterialsRequest(object): :param dict encryption_context: Encryption context to provide to master keys for underlying decrypt requests """ - algorithm = attr.ib(validator=attr.validators.instance_of(Algorithm)) - encrypted_data_keys = attr.ib(validator=attr.validators.instance_of(set)) - encryption_context = attr.ib(validator=attr.validators.instance_of(dict)) + algorithm = attr.ib(validator=instance_of(Algorithm)) + encrypted_data_keys = attr.ib(validator=deep_iterable(member_validator=instance_of(EncryptedDataKey))) + encryption_context = attr.ib( + validator=deep_mapping( + key_validator=instance_of(six.string_types), value_validator=instance_of(six.string_types) + ) + ) -@attr.s(hash=False) -class DecryptionMaterials(object): +_DEFAULT_SENTINEL = object() + + +@attr.s(hash=False, init=False) +class DecryptionMaterials(CryptographicMaterials): """Decryption materials returned by a crypto material manager's `decrypt_materials` method. .. versionadded:: 1.3.0 - :param data_key: Plaintext data key to use with message decryption - :type data_key: aws_encryption_sdk.structures.DataKey - :param bytes verification_key: Raw signature verification key + .. versionadded:: 1.5.0 + + The **algorithm**, **data_encryption_key**, **encryption_context**, and **keyring_trace** parameters. + + .. versionadded:: 1.5.0 + + All parameters are now optional. + + :param Algorithm algorithm: Algorithm to use for encrypting message (optional) + :param RawDataKey data_encryption_key: Plaintext data key to use for encrypting message (optional) + :param dict encryption_context: Encryption context tied to `encrypted_data_keys` (optional) + :param bytes verification_key: Raw signature verification key (optional) + :param keyring_trace: Any KeyRing trace entries (optional) + :type keyring_trace: list of :class:`KeyringTrace` """ - data_key = attr.ib(validator=attr.validators.instance_of(DataKey)) - verification_key = attr.ib(default=None, validator=attr.validators.optional(attr.validators.instance_of(bytes))) + verification_key = attr.ib(default=None, repr=False, validator=optional(instance_of(bytes))) + + def __init__( + self, data_key=_DEFAULT_SENTINEL, verification_key=None, **kwargs + ): # noqa we define this in the class docstring + + legacy_data_key_set = data_key is not _DEFAULT_SENTINEL + data_encryption_key_set = "data_encryption_key" in kwargs + + if legacy_data_key_set and data_encryption_key_set: + raise TypeError("Either data_key or data_encryption_key can be used but not both") + + if legacy_data_key_set and not data_encryption_key_set: + kwargs["data_encryption_key"] = data_key + + for legacy_missing in ("algorithm", "encryption_context"): + if legacy_missing not in kwargs: + kwargs[legacy_missing] = None + + super(DecryptionMaterials, self).__init__(**kwargs) + + self._setattr("verification_key", verification_key) + attr.validate(self) + + def __copy__(self): + # type: () -> DecryptionMaterials + """Do a shallow copy of this instance.""" + return DecryptionMaterials( + algorithm=self.algorithm, + data_encryption_key=self.data_encryption_key, + encryption_context=copy.copy(self.encryption_context), + verification_key=self.verification_key, + keyring_trace=copy.copy(self._keyring_trace), + ) + + @property + def is_complete(self): + # type: () -> bool + """Determine whether these materials are sufficiently complete for use as decryption materials. + + :rtype: bool + """ + if None in (self.algorithm, self.encryption_context): + return False + + if self.data_encryption_key is None: + return False + + if self.algorithm.signing_algorithm_info is not None and self.verification_key is None: + return False + + return True + + @property + def data_key(self): + # type: () -> RawDataKey + """Backwards-compatible shim for access to data key.""" + return self.data_encryption_key + + def with_data_encryption_key(self, data_encryption_key, keyring_trace): + # type: (Union[DataKey, RawDataKey], KeyringTrace) -> DecryptionMaterials + """Get new decryption materials that also include this data encryption key. + + .. versionadded:: 1.5.0 + + :param RawDataKey data_encryption_key: Data encryption key + :param KeyringTrace keyring_trace: Trace of actions that a keyring performed + while getting this data encryption key + :rtype: DecryptionMaterials + :raises AttributeError: if data encryption key is already set + :raises InvalidKeyringTraceError: if keyring trace does not match decrypt action + :raises InvalidKeyringTraceError: if keyring trace does not match data key provider + :raises InvalidDataKeyError: if data key length does not match algorithm suite + """ + if self.algorithm is None: + raise AttributeError("Algorithm is not set") + + return self._with_data_encryption_key( + data_encryption_key=data_encryption_key, + keyring_trace=keyring_trace, + required_flags={KeyringTraceFlag.DECRYPTED_DATA_KEY}, + ) + + def with_verification_key(self, verification_key): + # type: (bytes) -> DecryptionMaterials + """Get new decryption materials that also include this verification key. + + .. versionadded:: 1.5.0 + + :param bytes verification_key: Verification key + :rtype: DecryptionMaterials + """ + if self.verification_key is not None: + raise AttributeError("Verification key is already set.") + + if self.algorithm.signing_algorithm_info is None: + raise SignatureKeyError("Algorithm suite does not support signing keys.") + + new_materials = copy.copy(self) + + # Verify that the verification key matches the algorithm + Verifier.from_key_bytes(algorithm=new_materials.algorithm, key_bytes=verification_key) + + new_materials._setattr( # simplify access to copies pylint: disable=protected-access + "verification_key", verification_key + ) + + return new_materials diff --git a/src/aws_encryption_sdk/materials_managers/caching.py b/src/aws_encryption_sdk/materials_managers/caching.py index 992a39a7a..5a8e9e9bd 100644 --- a/src/aws_encryption_sdk/materials_managers/caching.py +++ b/src/aws_encryption_sdk/materials_managers/caching.py @@ -1,32 +1,25 @@ -# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). You -# may not use this file except in compliance with the License. A copy of -# the License is located at -# -# http://aws.amazon.com/apache2.0/ -# -# or in the "license" file accompanying this file. This file is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF -# ANY KIND, either express or implied. See the License for the specific -# language governing permissions and limitations under the License. +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 """Caching crypto material manager.""" import logging import uuid import attr import six +from attr.validators import instance_of, optional -from ..caches import ( +from aws_encryption_sdk.caches import ( CryptoMaterialsCacheEntryHints, build_decryption_materials_cache_key, build_encryption_materials_cache_key, ) -from ..caches.base import CryptoMaterialsCache -from ..exceptions import CacheKeyError -from ..internal.defaults import MAX_BYTES_PER_KEY, MAX_MESSAGES_PER_KEY -from ..internal.str_ops import to_bytes -from ..key_providers.base import MasterKeyProvider +from aws_encryption_sdk.caches.base import CryptoMaterialsCache +from aws_encryption_sdk.exceptions import CacheKeyError +from aws_encryption_sdk.internal.defaults import MAX_BYTES_PER_KEY, MAX_MESSAGES_PER_KEY +from aws_encryption_sdk.internal.str_ops import to_bytes +from aws_encryption_sdk.key_providers.base import MasterKeyProvider +from aws_encryption_sdk.keyrings.base import Keyring + from . import EncryptionMaterialsRequest from .base import CryptoMaterialsManager from .default import DefaultCryptoMaterialsManager @@ -36,10 +29,14 @@ @attr.s(hash=False) class CachingCryptoMaterialsManager(CryptoMaterialsManager): + # pylint: disable=too-many-instance-attributes """Crypto material manager which caches results from an underlying material manager. .. versionadded:: 1.3.0 + .. versionadded:: 1.5.0 + The *keyring* parameter. + >>> import aws_encryption_sdk >>> kms_key_provider = aws_encryption_sdk.KMSMasterKeyProvider(key_ids=[ ... 'arn:aws:kms:us-east-1:2222222222222:key/22222222-2222-2222-2222-222222222222', @@ -59,17 +56,16 @@ class CachingCryptoMaterialsManager(CryptoMaterialsManager): value. If no partition name is provided, a random UUID will be used. .. note:: - Either `backing_materials_manager` or `master_key_provider` must be provided. - `backing_materials_manager` will always be used if present. - - :param cache: Crypto cache to use with material manager - :type cache: aws_encryption_sdk.caches.base.CryptoMaterialsCache - :param backing_materials_manager: Crypto material manager to back this caching material manager - (either `backing_materials_manager` or `master_key_provider` required) - :type backing_materials_manager: aws_encryption_sdk.materials_managers.base.CryptoMaterialsManager - :param master_key_provider: Master key provider to use (either `backing_materials_manager` or - `master_key_provider` required) - :type master_key_provider: aws_encryption_sdk.key_providers.base.MasterKeyProvider + Exactly one of ``backing_materials_manager``, ``keyring``, or ``master_key_provider`` must be provided. + + :param CryptoMaterialsCache cache: Crypto cache to use with material manager + :param CryptoMaterialsManager backing_materials_manager: + Crypto material manager to back this caching material manager + (either ``backing_materials_manager``, ``keyring``, or ``master_key_provider`` required) + :param MasterKeyProvider master_key_provider: Master key provider to use + (either ``backing_materials_manager``, ``keyring``, or ``master_key_provider`` required) + :param Keyring keyring: Keyring to use + (either ``backing_materials_manager``, ``keyring``, or ``master_key_provider`` required) :param float max_age: Maximum time in seconds that a cache entry may be kept in the cache :param int max_messages_encrypted: Maximum number of messages that may be encrypted under a cache entry (optional) @@ -78,21 +74,14 @@ class CachingCryptoMaterialsManager(CryptoMaterialsManager): :param bytes partition_name: Partition name to use for this instance (optional) """ - cache = attr.ib(validator=attr.validators.instance_of(CryptoMaterialsCache)) - max_age = attr.ib(validator=attr.validators.instance_of(float)) - max_messages_encrypted = attr.ib( - default=MAX_MESSAGES_PER_KEY, validator=attr.validators.instance_of(six.integer_types) - ) - max_bytes_encrypted = attr.ib(default=MAX_BYTES_PER_KEY, validator=attr.validators.instance_of(six.integer_types)) - partition_name = attr.ib( - default=None, converter=to_bytes, validator=attr.validators.optional(attr.validators.instance_of(bytes)) - ) - master_key_provider = attr.ib( - default=None, validator=attr.validators.optional(attr.validators.instance_of(MasterKeyProvider)) - ) - backing_materials_manager = attr.ib( - default=None, validator=attr.validators.optional(attr.validators.instance_of(CryptoMaterialsManager)) - ) + cache = attr.ib(validator=instance_of(CryptoMaterialsCache)) + max_age = attr.ib(validator=instance_of(float)) + max_messages_encrypted = attr.ib(default=MAX_MESSAGES_PER_KEY, validator=instance_of(six.integer_types)) + max_bytes_encrypted = attr.ib(default=MAX_BYTES_PER_KEY, validator=instance_of(six.integer_types)) + partition_name = attr.ib(default=None, converter=to_bytes, validator=optional(instance_of(bytes))) + master_key_provider = attr.ib(default=None, validator=optional(instance_of(MasterKeyProvider))) + backing_materials_manager = attr.ib(default=None, validator=optional(instance_of(CryptoMaterialsManager))) + keyring = attr.ib(default=None, validator=optional(instance_of(Keyring))) def __attrs_post_init__(self): """Applies post-processing which cannot be handled by attrs.""" @@ -111,10 +100,21 @@ def __attrs_post_init__(self): if self.max_age <= 0.0: raise ValueError("max_age cannot be less than or equal to 0") + options_provided = [ + option is not None for option in (self.backing_materials_manager, self.keyring, self.master_key_provider) + ] + provided_count = len([is_set for is_set in options_provided if is_set]) + + if provided_count != 1: + raise TypeError("Exactly one of 'backing_materials_manager', 'keyring', or 'key_provider' must be provided") + if self.backing_materials_manager is None: - if self.master_key_provider is None: - raise TypeError("Either backing_materials_manager or master_key_provider must be defined") - self.backing_materials_manager = DefaultCryptoMaterialsManager(self.master_key_provider) + if self.master_key_provider is not None: + self.backing_materials_manager = DefaultCryptoMaterialsManager( + master_key_provider=self.master_key_provider + ) + else: + self.backing_materials_manager = DefaultCryptoMaterialsManager(keyring=self.keyring) if self.partition_name is None: self.partition_name = to_bytes(str(uuid.uuid4())) diff --git a/src/aws_encryption_sdk/materials_managers/default.py b/src/aws_encryption_sdk/materials_managers/default.py index 6d10465a9..336e5243f 100644 --- a/src/aws_encryption_sdk/materials_managers/default.py +++ b/src/aws_encryption_sdk/materials_managers/default.py @@ -1,29 +1,21 @@ -# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). You -# may not use this file except in compliance with the License. A copy of -# the License is located at -# -# http://aws.amazon.com/apache2.0/ -# -# or in the "license" file accompanying this file. This file is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF -# ANY KIND, either express or implied. See the License for the specific -# language governing permissions and limitations under the License. +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 """Default crypto material manager class.""" import logging import attr +from attr.validators import instance_of, optional -from ..exceptions import MasterKeyProviderError, SerializationError -from ..internal.crypto.authentication import Signer, Verifier -from ..internal.crypto.elliptic_curve import generate_ecc_signing_key -from ..internal.defaults import ALGORITHM, ENCODED_SIGNER_KEY -from ..internal.str_ops import to_str -from ..internal.utils import prepare_data_keys -from ..key_providers.base import MasterKeyProvider -from . import DecryptionMaterials, EncryptionMaterials -from .base import CryptoMaterialsManager +from aws_encryption_sdk.exceptions import InvalidCryptographicMaterialsError, MasterKeyProviderError, SerializationError +from aws_encryption_sdk.internal.crypto.authentication import Signer, Verifier +from aws_encryption_sdk.internal.crypto.elliptic_curve import generate_ecc_signing_key +from aws_encryption_sdk.internal.defaults import ALGORITHM, ENCODED_SIGNER_KEY +from aws_encryption_sdk.internal.str_ops import to_str +from aws_encryption_sdk.internal.utils import prepare_data_keys +from aws_encryption_sdk.key_providers.base import MasterKeyProvider +from aws_encryption_sdk.keyrings.base import Keyring +from aws_encryption_sdk.materials_managers import DecryptionMaterials, EncryptionMaterials +from aws_encryption_sdk.materials_managers.base import CryptoMaterialsManager _LOGGER = logging.getLogger(__name__) @@ -34,12 +26,26 @@ class DefaultCryptoMaterialsManager(CryptoMaterialsManager): .. versionadded:: 1.3.0 - :param master_key_provider: Master key provider to use - :type master_key_provider: aws_encryption_sdk.key_providers.base.MasterKeyProvider + .. versionadded:: 1.5.0 + The *keyring* parameter. + + :param MasterKeyProvider master_key_provider: Master key provider to use + (either ``keyring`` or ``master_key_provider`` is required) + :param Keyring keyring: Keyring to use + (either ``keyring`` or ``master_key_provider`` is required) """ algorithm = ALGORITHM - master_key_provider = attr.ib(validator=attr.validators.instance_of(MasterKeyProvider)) + master_key_provider = attr.ib(default=None, validator=optional(instance_of(MasterKeyProvider))) + keyring = attr.ib(default=None, validator=optional(instance_of(Keyring))) + + def __attrs_post_init__(self): + """Apply input requirements.""" + both = self.master_key_provider is not None and self.keyring is not None + neither = self.master_key_provider is None and self.keyring is None + + if both or neither: + raise TypeError("Exactly one of 'keyring' or 'master_key_provider' must be supplied.") def _generate_signing_key_and_update_encryption_context(self, algorithm, encryption_context): """Generates a signing key based on the provided algorithm. @@ -58,7 +64,7 @@ def _generate_signing_key_and_update_encryption_context(self, algorithm, encrypt encryption_context[ENCODED_SIGNER_KEY] = to_str(signer.encoded_public_key()) return signer.key_bytes() - def get_encryption_materials(self, request): + def _get_encryption_materials_using_master_key_provider(self, request): """Creates encryption materials using underlying master key provider. :param request: encryption materials request @@ -101,6 +107,64 @@ def get_encryption_materials(self, request): signing_key=signing_key, ) + def _get_encryption_materials_using_keyring(self, request): + """Creates encryption materials using underlying keyring. + + :param request: encryption materials request + :type request: aws_encryption_sdk.materials_managers.EncryptionMaterialsRequest + :returns: encryption materials + :rtype: aws_encryption_sdk.materials_managers.EncryptionMaterials + :raises InvalidCryptographicMaterialsError: if keyring cannot complete encryption materials + :raises InvalidCryptographicMaterialsError: + if encryption materials received from keyring do not match request + """ + algorithm = request.algorithm if request.algorithm is not None else self.algorithm + encryption_context = request.encryption_context.copy() + + signing_key = self._generate_signing_key_and_update_encryption_context(algorithm, encryption_context) + + expected_encryption_context = encryption_context.copy() + + encryption_materials = EncryptionMaterials( + algorithm=algorithm, encryption_context=encryption_context, signing_key=signing_key, + ) + + final_materials = self.keyring.on_encrypt(encryption_materials=encryption_materials) + + materials_are_valid = ( + final_materials.algorithm is algorithm, + final_materials.encryption_context == expected_encryption_context, + final_materials.signing_key is signing_key, + ) + if not all(materials_are_valid): + raise InvalidCryptographicMaterialsError("Encryption materials do not match request!") + + if not final_materials.is_complete: + raise InvalidCryptographicMaterialsError("Encryption materials are incomplete!") + + _LOGGER.debug("Post-encrypt encryption context: %s", encryption_context) + + return final_materials + + def get_encryption_materials(self, request): + """Creates encryption materials using underlying master key provider. + + :param request: encryption materials request + :type request: aws_encryption_sdk.materials_managers.EncryptionMaterialsRequest + :returns: encryption materials + :rtype: aws_encryption_sdk.materials_managers.EncryptionMaterials + :raises InvalidCryptographicMaterialsError: if keyring cannot complete encryption materials + :raises InvalidCryptographicMaterialsError: + if encryption materials received from keyring do not match request + :raises MasterKeyProviderError: if no master keys are available from the underlying master key provider + :raises MasterKeyProviderError: if the primary master key provided by the underlying master key provider + is not included in the full set of master keys provided by that provider + """ + if self.master_key_provider is not None: + return self._get_encryption_materials_using_master_key_provider(request) + + return self._get_encryption_materials_using_keyring(request) + def _load_verification_key_from_encryption_context(self, algorithm, encryption_context): """Loads the verification key from the encryption context if used by algorithm suite. @@ -124,7 +188,7 @@ def _load_verification_key_from_encryption_context(self, algorithm, encryption_c verifier = Verifier.from_encoded_point(algorithm=algorithm, encoded_point=encoded_verification_key) return verifier.key_bytes() - def decrypt_materials(self, request): + def _decrypt_materials_using_master_key_provider(self, request): """Obtains a plaintext data key from one or more encrypted data keys using underlying master key provider. @@ -143,3 +207,55 @@ def decrypt_materials(self, request): ) return DecryptionMaterials(data_key=data_key, verification_key=verification_key) + + def _decrypt_materials_using_keyring(self, request): + """Obtains a plaintext data key from one or more encrypted data keys + using underlying keyring. + + :param request: decrypt materials request + :type request: aws_encryption_sdk.materials_managers.DecryptionMaterialsRequest + :returns: decryption materials + :rtype: aws_encryption_sdk.materials_managers.DecryptionMaterials + :raises InvalidCryptographicMaterialsError: if keyring cannot complete decryption materials + :raises InvalidCryptographicMaterialsError: + if decryption materials received from keyring do not match request + """ + verification_key = self._load_verification_key_from_encryption_context( + algorithm=request.algorithm, encryption_context=request.encryption_context + ) + decryption_materials = DecryptionMaterials( + algorithm=request.algorithm, + encryption_context=request.encryption_context.copy(), + verification_key=verification_key, + ) + + final_materials = self.keyring.on_decrypt( + decryption_materials=decryption_materials, encrypted_data_keys=request.encrypted_data_keys + ) + + materials_are_valid = ( + final_materials.algorithm is request.algorithm, + final_materials.encryption_context == request.encryption_context, + final_materials.verification_key is verification_key, + ) + if not all(materials_are_valid): + raise InvalidCryptographicMaterialsError("Decryption materials do not match request!") + + if not final_materials.is_complete: + raise InvalidCryptographicMaterialsError("Decryption materials are incomplete!") + + return final_materials + + def decrypt_materials(self, request): + """Obtains a plaintext data key from one or more encrypted data keys + using underlying master key provider. + + :param request: decrypt materials request + :type request: aws_encryption_sdk.materials_managers.DecryptionMaterialsRequest + :returns: decryption materials + :rtype: aws_encryption_sdk.materials_managers.DecryptionMaterials + """ + if self.master_key_provider is not None: + return self._decrypt_materials_using_master_key_provider(request) + + return self._decrypt_materials_using_keyring(request) diff --git a/src/aws_encryption_sdk/streaming_client.py b/src/aws_encryption_sdk/streaming_client.py index 504f68977..ff549563d 100644 --- a/src/aws_encryption_sdk/streaming_client.py +++ b/src/aws_encryption_sdk/streaming_client.py @@ -1,15 +1,5 @@ -# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). You -# may not use this file except in compliance with the License. A copy of -# the License is located at -# -# http://aws.amazon.com/apache2.0/ -# -# or in the "license" file accompanying this file. This file is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF -# ANY KIND, either express or implied. See the License for the specific -# language governing permissions and limitations under the License. +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 """High level AWS Encryption SDK client for streaming objects.""" from __future__ import division @@ -20,6 +10,7 @@ import attr import six +from attr.validators import instance_of, optional import aws_encryption_sdk.internal.utils from aws_encryption_sdk.exceptions import ( @@ -54,6 +45,7 @@ serialize_non_framed_open, ) from aws_encryption_sdk.key_providers.base import MasterKeyProvider +from aws_encryption_sdk.keyrings.base import Keyring from aws_encryption_sdk.materials_managers import DecryptionMaterialsRequest, EncryptionMaterialsRequest from aws_encryption_sdk.materials_managers.base import CryptoMaterialsManager from aws_encryption_sdk.materials_managers.default import DefaultCryptoMaterialsManager @@ -67,14 +59,19 @@ class _ClientConfig(object): """Parent configuration object for StreamEncryptor and StreamDecryptor objects. + .. versionadded:: 1.5.0 + The *keyring* parameter. + :param source: Source data to encrypt or decrypt :type source: str, bytes, io.IOBase, or file - :param materials_manager: `CryptoMaterialsManager` from which to obtain cryptographic materials - (either `materials_manager` or `key_provider` required) - :type materials_manager: aws_encryption_sdk.materials_manager.base.CryptoMaterialsManager - :param key_provider: `MasterKeyProvider` from which to obtain data keys for encryption - (either `materials_manager` or `key_provider` required) - :type key_provider: aws_encryption_sdk.key_providers.base.MasterKeyProvider + :param CryptoMaterialsManager materials_manager: + Cryptographic materials manager to use for encryption + (either ``materials_manager``, ``keyring``, ``key_provider`` required) + :param Keyring keyring: Keyring to use for encryption + (either ``materials_manager``, ``keyring``, ``key_provider`` required) + :param MasterKeyProvider key_provider: + Master key provider to use for encryption + (either ``materials_manager``, ``keyring``, ``key_provider`` required) :param int source_length: Length of source data (optional) .. note:: @@ -83,28 +80,27 @@ class _ClientConfig(object): """ source = attr.ib(hash=True, converter=aws_encryption_sdk.internal.utils.prep_stream_data) - materials_manager = attr.ib( - hash=True, default=None, validator=attr.validators.optional(attr.validators.instance_of(CryptoMaterialsManager)) - ) - key_provider = attr.ib( - hash=True, default=None, validator=attr.validators.optional(attr.validators.instance_of(MasterKeyProvider)) - ) - source_length = attr.ib( - hash=True, default=None, validator=attr.validators.optional(attr.validators.instance_of(six.integer_types)) - ) + materials_manager = attr.ib(hash=True, default=None, validator=optional(instance_of(CryptoMaterialsManager))) + keyring = attr.ib(default=None, validator=optional(instance_of(Keyring))) + key_provider = attr.ib(hash=True, default=None, validator=optional(instance_of(MasterKeyProvider))) + source_length = attr.ib(hash=True, default=None, validator=optional(instance_of(six.integer_types))) line_length = attr.ib( - hash=True, default=LINE_LENGTH, validator=attr.validators.instance_of(six.integer_types) + hash=True, default=LINE_LENGTH, validator=instance_of(six.integer_types) ) # DEPRECATED: Value is no longer configurable here. Parameter left here to avoid breaking consumers. def __attrs_post_init__(self): """Normalize inputs to crypto material manager.""" - both_cmm_and_mkp_defined = self.materials_manager is not None and self.key_provider is not None - neither_cmm_nor_mkp_defined = self.materials_manager is None and self.key_provider is None + options_provided = [option is not None for option in (self.materials_manager, self.keyring, self.key_provider)] + provided_count = len([is_set for is_set in options_provided if is_set]) + + if provided_count != 1: + raise TypeError("Exactly one of 'materials_manager', 'keyring', or 'key_provider' must be provided") - if both_cmm_and_mkp_defined or neither_cmm_nor_mkp_defined: - raise TypeError("Exactly one of materials_manager or key_provider must be provided") if self.materials_manager is None: - self.materials_manager = DefaultCryptoMaterialsManager(master_key_provider=self.key_provider) + if self.key_provider is not None: + self.materials_manager = DefaultCryptoMaterialsManager(master_key_provider=self.key_provider) + else: + self.materials_manager = DefaultCryptoMaterialsManager(keyring=self.keyring) class _EncryptionStream(io.IOBase): @@ -141,6 +137,7 @@ class _EncryptionStream(io.IOBase): _message_prepped = None # type: bool source_stream = None _stream_length = None # type: int + keyring_trace = () def __new__(cls, **kwargs): """Perform necessary handling for _EncryptionStream instances that should be @@ -311,14 +308,19 @@ def next(self): class EncryptorConfig(_ClientConfig): """Configuration object for StreamEncryptor class. + .. versionadded:: 1.5.0 + The *keyring* parameter. + :param source: Source data to encrypt or decrypt :type source: str, bytes, io.IOBase, or file - :param materials_manager: `CryptoMaterialsManager` from which to obtain cryptographic materials - (either `materials_manager` or `key_provider` required) - :type materials_manager: aws_encryption_sdk.materials_manager.base.CryptoMaterialsManager - :param key_provider: `MasterKeyProvider` from which to obtain data keys for encryption - (either `materials_manager` or `key_provider` required) - :type key_provider: aws_encryption_sdk.key_providers.base.MasterKeyProvider + :param CryptoMaterialsManager materials_manager: + Cryptographic materials manager to use for encryption + (either ``materials_manager``, ``keyring``, ``key_provider`` required) + :param Keyring keyring: Keyring to use for encryption + (either ``materials_manager``, ``keyring``, ``key_provider`` required) + :param MasterKeyProvider key_provider: + Master key provider to use for encryption + (either ``materials_manager``, ``keyring``, ``key_provider`` required) :param int source_length: Length of source data (optional) .. note:: @@ -360,16 +362,21 @@ class StreamEncryptor(_EncryptionStream): # pylint: disable=too-many-instance-a .. note:: If config is provided, all other parameters are ignored. + .. versionadded:: 1.5.0 + The *keyring* parameter. + :param config: Client configuration object (config or individual parameters required) :type config: aws_encryption_sdk.streaming_client.EncryptorConfig :param source: Source data to encrypt or decrypt :type source: str, bytes, io.IOBase, or file - :param materials_manager: `CryptoMaterialsManager` from which to obtain cryptographic materials - (either `materials_manager` or `key_provider` required) - :type materials_manager: aws_encryption_sdk.materials_manager.base.CryptoMaterialsManager - :param key_provider: `MasterKeyProvider` from which to obtain data keys for encryption - (either `materials_manager` or `key_provider` required) - :type key_provider: aws_encryption_sdk.key_providers.base.MasterKeyProvider + :param CryptoMaterialsManager materials_manager: + Cryptographic materials manager to use for encryption + (either ``materials_manager``, ``keyring``, ``key_provider`` required) + :param Keyring keyring: Keyring to use for encryption + (either ``materials_manager``, ``keyring``, ``key_provider`` required) + :param MasterKeyProvider key_provider: + Master key provider to use for encryption + (either ``materials_manager``, ``keyring``, ``key_provider`` required) :param int source_length: Length of source data (optional) .. note:: @@ -437,6 +444,7 @@ def _prep_message(self): self._encryption_materials = self.config.materials_manager.get_encryption_materials( request=encryption_materials_request ) + self.keyring_trace = self._encryption_materials.keyring_trace if self.config.algorithm is not None and self._encryption_materials.algorithm != self.config.algorithm: raise ActionNotAllowedError( @@ -667,14 +675,19 @@ def close(self): class DecryptorConfig(_ClientConfig): """Configuration object for StreamDecryptor class. + .. versionadded:: 1.5.0 + The *keyring* parameter. + :param source: Source data to encrypt or decrypt :type source: str, bytes, io.IOBase, or file - :param materials_manager: `CryptoMaterialsManager` from which to obtain cryptographic materials - (either `materials_manager` or `key_provider` required) - :type materials_manager: aws_encryption_sdk.materials_managers.base.CryptoMaterialsManager - :param key_provider: `MasterKeyProvider` from which to obtain data keys for decryption - (either `materials_manager` or `key_provider` required) - :type key_provider: aws_encryption_sdk.key_providers.base.MasterKeyProvider + :param CryptoMaterialsManager materials_manager: + Cryptographic materials manager to use for encryption + (either ``materials_manager``, ``keyring``, ``key_provider`` required) + :param Keyring keyring: Keyring to use for encryption + (either ``materials_manager``, ``keyring``, ``key_provider`` required) + :param MasterKeyProvider key_provider: + Master key provider to use for encryption + (either ``materials_manager``, ``keyring``, ``key_provider`` required) :param int source_length: Length of source data (optional) .. note:: @@ -701,16 +714,21 @@ class StreamDecryptor(_EncryptionStream): # pylint: disable=too-many-instance-a .. note:: If config is provided, all other parameters are ignored. + .. versionadded:: 1.5.0 + The *keyring* parameter. + :param config: Client configuration object (config or individual parameters required) :type config: aws_encryption_sdk.streaming_client.DecryptorConfig :param source: Source data to encrypt or decrypt :type source: str, bytes, io.IOBase, or file - :param materials_manager: `CryptoMaterialsManager` from which to obtain cryptographic materials - (either `materials_manager` or `key_provider` required) - :type materials_manager: aws_encryption_sdk.materials_managers.base.CryptoMaterialsManager - :param key_provider: `MasterKeyProvider` from which to obtain data keys for decryption - (either `materials_manager` or `key_provider` required) - :type key_provider: aws_encryption_sdk.key_providers.base.MasterKeyProvider + :param CryptoMaterialsManager materials_manager: + Cryptographic materials manager to use for encryption + (either ``materials_manager``, ``keyring``, ``key_provider`` required) + :param Keyring keyring: Keyring to use for encryption + (either ``materials_manager``, ``keyring``, ``key_provider`` required) + :param MasterKeyProvider key_provider: + Master key provider to use for encryption + (either ``materials_manager``, ``keyring``, ``key_provider`` required) :param int source_length: Length of source data (optional) .. note:: @@ -763,6 +781,8 @@ def _read_header(self): encryption_context=header.encryption_context, ) decryption_materials = self.config.materials_manager.decrypt_materials(request=decrypt_materials_request) + self.keyring_trace = decryption_materials.keyring_trace + if decryption_materials.verification_key is None: self.verifier = None else: diff --git a/src/aws_encryption_sdk/structures.py b/src/aws_encryption_sdk/structures.py index 97e4c1d13..4e8275a2c 100644 --- a/src/aws_encryption_sdk/structures.py +++ b/src/aws_encryption_sdk/structures.py @@ -11,59 +11,59 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. """Public data structures for aws_encryption_sdk.""" +import copy + import attr import six +from attr.validators import deep_iterable, deep_mapping, instance_of, optional -import aws_encryption_sdk.identifiers +from aws_encryption_sdk.identifiers import Algorithm, ContentType, KeyringTraceFlag, ObjectType, SerializationVersion from aws_encryption_sdk.internal.str_ops import to_bytes, to_str +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Tuple # noqa pylint: disable=unused-import +except ImportError: # pragma: no cover + # We only actually need these imports when running the mypy checks + pass + @attr.s(hash=True) -class MessageHeader(object): - # pylint: disable=too-many-instance-attributes - """Deserialized message header object. +class MasterKeyInfo(object): + """Contains information necessary to identify a Master Key. - :param version: Message format version, per spec - :type version: aws_encryption_sdk.identifiers.SerializationVersion - :param type: Message content type, per spec - :type type: aws_encryption_sdk.identifiers.ObjectType - :param algorithm: Algorithm to use for encryption - :type algorithm: aws_encryption_sdk.identifiers.Algorithm - :param bytes message_id: Message ID - :param dict encryption_context: Dictionary defining encryption context - :param encrypted_data_keys: Encrypted data keys - :type encrypted_data_keys: set of :class:`aws_encryption_sdk.structures.EncryptedDataKey` - :param content_type: Message content framing type (framed/non-framed) - :type content_type: aws_encryption_sdk.identifiers.ContentType - :param bytes content_aad_length: empty - :param int header_iv_length: Bytes in Initialization Vector value found in header - :param int frame_length: Length of message frame in bytes - """ + .. note:: - version = attr.ib( - hash=True, validator=attr.validators.instance_of(aws_encryption_sdk.identifiers.SerializationVersion) - ) - type = attr.ib(hash=True, validator=attr.validators.instance_of(aws_encryption_sdk.identifiers.ObjectType)) - algorithm = attr.ib(hash=True, validator=attr.validators.instance_of(aws_encryption_sdk.identifiers.Algorithm)) - message_id = attr.ib(hash=True, validator=attr.validators.instance_of(bytes)) - encryption_context = attr.ib(hash=True, validator=attr.validators.instance_of(dict)) - encrypted_data_keys = attr.ib(hash=True, validator=attr.validators.instance_of(set)) - content_type = attr.ib(hash=True, validator=attr.validators.instance_of(aws_encryption_sdk.identifiers.ContentType)) - content_aad_length = attr.ib(hash=True, validator=attr.validators.instance_of(six.integer_types)) - header_iv_length = attr.ib(hash=True, validator=attr.validators.instance_of(six.integer_types)) - frame_length = attr.ib(hash=True, validator=attr.validators.instance_of(six.integer_types)) + The only keyring or master key that should need to set ``key_name`` is the Raw AES keyring/master key. + For all other keyrings and master keys, ``key_info`` and ``key_name`` should always be the same. -@attr.s(hash=True) -class MasterKeyInfo(object): - """Contains information necessary to identify a Master Key. + .. versionadded:: 1.5.0 + ``key_name`` :param str provider_id: MasterKey provider_id value :param bytes key_info: MasterKey key_info value + :param bytes key_name: Key name if different than key_info (optional) """ - provider_id = attr.ib(hash=True, validator=attr.validators.instance_of((six.string_types, bytes)), converter=to_str) - key_info = attr.ib(hash=True, validator=attr.validators.instance_of((six.string_types, bytes)), converter=to_bytes) + provider_id = attr.ib(hash=True, validator=instance_of((six.string_types, bytes)), converter=to_str) + key_info = attr.ib(hash=True, validator=instance_of((six.string_types, bytes)), converter=to_bytes) + key_name = attr.ib( + hash=True, default=None, validator=optional(instance_of((six.string_types, bytes))), converter=to_bytes + ) + + def __attrs_post_init__(self): + """Set ``key_name`` if not already set.""" + if self.key_name is None: + self.key_name = self.key_info + + @property + def key_namespace(self): + """Access the key namespace value (previously, provider ID). + + .. versionadded:: 1.5.0 + + """ + return self.provider_id @attr.s(hash=True) @@ -75,8 +75,20 @@ class RawDataKey(object): :param bytes data_key: Plaintext data key """ - key_provider = attr.ib(hash=True, validator=attr.validators.instance_of(MasterKeyInfo)) - data_key = attr.ib(hash=True, repr=False, validator=attr.validators.instance_of(bytes)) + key_provider = attr.ib(hash=True, validator=instance_of(MasterKeyInfo)) + data_key = attr.ib(hash=True, repr=False, validator=instance_of(bytes)) + + @classmethod + def from_data_key(cls, data_key): + # type: (DataKey) -> RawDataKey + """Build an :class:`RawDataKey` from a :class:`DataKey`. + + .. versionadded:: 1.5.0 + """ + if not isinstance(data_key, DataKey): + raise TypeError("data_key must be type DataKey not {}".format(type(data_key).__name__)) + + return RawDataKey(key_provider=copy.copy(data_key.key_provider), data_key=copy.copy(data_key.data_key)) @attr.s(hash=True) @@ -89,9 +101,9 @@ class DataKey(object): :param bytes encrypted_data_key: Encrypted data key """ - key_provider = attr.ib(hash=True, validator=attr.validators.instance_of(MasterKeyInfo)) - data_key = attr.ib(hash=True, repr=False, validator=attr.validators.instance_of(bytes)) - encrypted_data_key = attr.ib(hash=True, validator=attr.validators.instance_of(bytes)) + key_provider = attr.ib(hash=True, validator=instance_of(MasterKeyInfo)) + data_key = attr.ib(hash=True, repr=False, validator=instance_of(bytes)) + encrypted_data_key = attr.ib(hash=True, validator=instance_of(bytes)) @attr.s(hash=True) @@ -103,5 +115,105 @@ class EncryptedDataKey(object): :param bytes encrypted_data_key: Encrypted data key """ - key_provider = attr.ib(hash=True, validator=attr.validators.instance_of(MasterKeyInfo)) - encrypted_data_key = attr.ib(hash=True, validator=attr.validators.instance_of(bytes)) + key_provider = attr.ib(hash=True, validator=instance_of(MasterKeyInfo)) + encrypted_data_key = attr.ib(hash=True, validator=instance_of(bytes)) + + @classmethod + def from_data_key(cls, data_key): + # type: (DataKey) -> EncryptedDataKey + """Build an :class:`EncryptedDataKey` from a :class:`DataKey`. + + .. versionadded:: 1.5.0 + """ + if not isinstance(data_key, DataKey): + raise TypeError("data_key must be type DataKey not {}".format(type(data_key).__name__)) + + return EncryptedDataKey( + key_provider=copy.copy(data_key.key_provider), encrypted_data_key=copy.copy(data_key.encrypted_data_key) + ) + + +@attr.s +class KeyringTrace(object): + """Record of all actions that a KeyRing performed with a wrapping key. + + .. versionadded:: 1.5.0 + + :param MasterKeyInfo wrapping_key: Wrapping key used + :param Set[KeyringTraceFlag] flags: Actions performed + """ + + wrapping_key = attr.ib(validator=instance_of(MasterKeyInfo)) + flags = attr.ib(validator=deep_iterable(member_validator=instance_of(KeyringTraceFlag))) + + +@attr.s(hash=True) +class MessageHeader(object): + # pylint: disable=too-many-instance-attributes + """Deserialized message header object. + + :param SerializationVersion version: Message format version, per spec + :param ObjectType type: Message content type, per spec + :param AlgorithmSuite algorithm: Algorithm to use for encryption + :param bytes message_id: Message ID + :param Dict[str,str] encryption_context: Dictionary defining encryption context + :param Sequence[EncryptedDataKey] encrypted_data_keys: Encrypted data keys + :param ContentType content_type: Message content framing type (framed/non-framed) + :param int content_aad_length: empty + :param int header_iv_length: Bytes in Initialization Vector value found in header + :param int frame_length: Length of message frame in bytes + """ + + version = attr.ib(hash=True, validator=instance_of(SerializationVersion)) + type = attr.ib(hash=True, validator=instance_of(ObjectType)) + algorithm = attr.ib(hash=True, validator=instance_of(Algorithm)) + message_id = attr.ib(hash=True, validator=instance_of(bytes)) + encryption_context = attr.ib( + hash=True, + validator=deep_mapping( + key_validator=instance_of(six.string_types), value_validator=instance_of(six.string_types) + ), + ) + encrypted_data_keys = attr.ib(hash=True, validator=deep_iterable(member_validator=instance_of(EncryptedDataKey))) + content_type = attr.ib(hash=True, validator=instance_of(ContentType)) + content_aad_length = attr.ib(hash=True, validator=instance_of(six.integer_types)) + header_iv_length = attr.ib(hash=True, validator=instance_of(six.integer_types)) + frame_length = attr.ib(hash=True, validator=instance_of(six.integer_types)) + + +@attr.s +class CryptoResult(object): + """Result container for one-shot cryptographic API results. + + .. versionadded:: 1.5.0 + + .. note:: + + For backwards compatibility, + this container also unpacks like a 2-member tuple. + This allows for backwards compatibility with the previous outputs. + + :param bytes result: Binary results of the cryptographic operation + :param MessageHeader header: Encrypted message metadata + :param Tuple[KeyringTrace] keyring_trace: Keyring trace entries + """ + + result = attr.ib(validator=instance_of(bytes)) + header = attr.ib(validator=instance_of(MessageHeader)) + keyring_trace = attr.ib(validator=deep_iterable(member_validator=instance_of(KeyringTrace))) + + def __attrs_post_init__(self): + """Construct the inner tuple for backwards compatibility.""" + self._legacy_container = (self.result, self.header) + + def __len__(self): + """Emulate the inner tuple.""" + return self._legacy_container.__len__() + + def __iter__(self): + """Emulate the inner tuple.""" + return self._legacy_container.__iter__() + + def __getitem__(self, key): + """Emulate the inner tuple.""" + return self._legacy_container.__getitem__(key) diff --git a/src/pylintrc b/src/pylintrc index af00ced56..1722f208c 100644 --- a/src/pylintrc +++ b/src/pylintrc @@ -33,6 +33,9 @@ additional-builtins = raw_input [DESIGN] max-args = 10 +[SIMILARITIES] +ignore-imports = yes + [FORMAT] max-line-length = 120 diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test/functional/__init__.py b/test/functional/__init__.py index 53a960891..ad0e71d6c 100644 --- a/test/functional/__init__.py +++ b/test/functional/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"). You # may not use this file except in compliance with the License. A copy of @@ -10,3 +10,4 @@ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. +"""Dummy stub to make linters work better.""" diff --git a/test/functional/functional_test_utils.py b/test/functional/functional_test_utils.py new file mode 100644 index 000000000..3822ef5fa --- /dev/null +++ b/test/functional/functional_test_utils.py @@ -0,0 +1,29 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Utility functions to handle configuration and credentials setup for functional tests.""" +import boto3 +import pytest +from moto.kms import mock_kms + +FAKE_REGION = "us-west-2" + + +def _create_cmk(): + # type: () -> str + kms = boto3.client("kms", region_name=FAKE_REGION) + response = kms.create_key() + return response["KeyMetadata"]["Arn"] + + +@pytest.fixture +def fake_generator(): + with mock_kms(): + yield _create_cmk() + + +@pytest.fixture +def fake_generator_and_child(): + with mock_kms(): + generator = _create_cmk() + child = _create_cmk() + yield generator, child diff --git a/examples/test/test_i_one_kms_cmk.py b/test/functional/internal/__init__.py similarity index 52% rename from examples/test/test_i_one_kms_cmk.py rename to test/functional/internal/__init__.py index 71ce74d3d..ad0e71d6c 100644 --- a/examples/test/test_i_one_kms_cmk.py +++ b/test/functional/internal/__init__.py @@ -10,20 +10,4 @@ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. -"""Unit test suite for the encryption and decryption using one KMS CMK example.""" - -import botocore.session -import pytest - -from ..src.one_kms_cmk import encrypt_decrypt -from .examples_test_utils import get_cmk_arn -from .examples_test_utils import static_plaintext - - -pytestmark = [pytest.mark.examples] - - -def test_one_kms_cmk(): - plaintext = static_plaintext - cmk_arn = get_cmk_arn() - encrypt_decrypt(key_arn=cmk_arn, source_plaintext=plaintext, botocore_session=botocore.session.Session()) +"""Dummy stub to make linters work better.""" diff --git a/examples/test/test_i_one_kms_cmk_unsigned.py b/test/functional/internal/crypto/__init__.py similarity index 50% rename from examples/test/test_i_one_kms_cmk_unsigned.py rename to test/functional/internal/crypto/__init__.py index 8a2758c96..ad0e71d6c 100644 --- a/examples/test/test_i_one_kms_cmk_unsigned.py +++ b/test/functional/internal/crypto/__init__.py @@ -10,20 +10,4 @@ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. -"""Unit test suite for the encryption and decryption using one KMS CMK with an unsigned algorithm example.""" - -import botocore.session -import pytest - -from ..src.one_kms_cmk_unsigned import encrypt_decrypt -from .examples_test_utils import get_cmk_arn -from .examples_test_utils import static_plaintext - - -pytestmark = [pytest.mark.examples] - - -def test_one_kms_cmk_unsigned(): - plaintext = static_plaintext - cmk_arn = get_cmk_arn() - encrypt_decrypt(key_arn=cmk_arn, source_plaintext=plaintext, botocore_session=botocore.session.Session()) +"""Dummy stub to make linters work better.""" diff --git a/test/functional/test_f_crypto.py b/test/functional/internal/crypto/test_crypto.py similarity index 100% rename from test/functional/test_f_crypto.py rename to test/functional/internal/crypto/test_crypto.py diff --git a/test/functional/test_f_crypto_iv.py b/test/functional/internal/crypto/test_iv.py similarity index 100% rename from test/functional/test_f_crypto_iv.py rename to test/functional/internal/crypto/test_iv.py diff --git a/test/functional/keyrings/__init__.py b/test/functional/keyrings/__init__.py new file mode 100644 index 000000000..ad0e71d6c --- /dev/null +++ b/test/functional/keyrings/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Dummy stub to make linters work better.""" diff --git a/test/functional/keyrings/aws_kms/__init__.py b/test/functional/keyrings/aws_kms/__init__.py new file mode 100644 index 000000000..d548f9b1f --- /dev/null +++ b/test/functional/keyrings/aws_kms/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Dummy stub to make linters work better.""" diff --git a/test/functional/keyrings/aws_kms/test_aws_kms.py b/test/functional/keyrings/aws_kms/test_aws_kms.py new file mode 100644 index 000000000..5a5a82bd9 --- /dev/null +++ b/test/functional/keyrings/aws_kms/test_aws_kms.py @@ -0,0 +1,494 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Functional tests for ``aws_encryption_sdk.keyrings.aws_kms``.""" +import itertools +import logging +import os + +import boto3 +import pytest +from moto.kms import mock_kms + +from aws_encryption_sdk.exceptions import DecryptKeyError, EncryptKeyError +from aws_encryption_sdk.identifiers import KeyringTraceFlag +from aws_encryption_sdk.internal.defaults import ALGORITHM +from aws_encryption_sdk.keyrings.aws_kms import ( + KEY_NAMESPACE, + KmsKeyring, + _AwsKmsDiscoveryKeyring, + _AwsKmsSingleCmkKeyring, + _do_aws_kms_decrypt, + _do_aws_kms_encrypt, + _do_aws_kms_generate_data_key, + _try_aws_kms_decrypt, +) +from aws_encryption_sdk.keyrings.aws_kms.client_suppliers import DefaultClientSupplier +from aws_encryption_sdk.materials_managers import DecryptionMaterials, EncryptionMaterials +from aws_encryption_sdk.structures import EncryptedDataKey, KeyringTrace, MasterKeyInfo, RawDataKey + +# used as fixtures +from ...functional_test_utils import fake_generator # noqa pylint: disable=unused-import +from ...functional_test_utils import fake_generator_and_child # noqa pylint: disable=unused-import +from ...functional_test_utils import FAKE_REGION + +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Iterable, List # noqa pylint: disable=unused-import +except ImportError: # pragma: no cover + # We only actually need these imports when running the mypy checks + pass + +pytestmark = [pytest.mark.functional, pytest.mark.local] + + +def _matching_flags(wrapping_key, keyring_trace): + # type: (MasterKeyInfo, Iterable[KeyringTrace]) -> List[KeyringTraceFlag] + return list( + itertools.chain.from_iterable([entry.flags for entry in keyring_trace if entry.wrapping_key == wrapping_key]) + ) + + +def test_aws_kms_single_cmk_keyring_on_encrypt_empty_materials(fake_generator): + keyring = _AwsKmsSingleCmkKeyring(key_id=fake_generator, client_supplier=DefaultClientSupplier()) + + initial_materials = EncryptionMaterials(algorithm=ALGORITHM, encryption_context={}) + + result_materials = keyring.on_encrypt(initial_materials) + + assert result_materials.data_encryption_key is not None + assert len(result_materials.encrypted_data_keys) == 1 + + generator_flags = _matching_flags( + MasterKeyInfo(provider_id=KEY_NAMESPACE, key_info=fake_generator), result_materials.keyring_trace + ) + + assert KeyringTraceFlag.GENERATED_DATA_KEY in generator_flags + assert KeyringTraceFlag.ENCRYPTED_DATA_KEY in generator_flags + assert KeyringTraceFlag.SIGNED_ENCRYPTION_CONTEXT in generator_flags + + +def test_aws_kms_single_cmk_keyring_on_encrypt_existing_data_key(fake_generator): + keyring = _AwsKmsSingleCmkKeyring(key_id=fake_generator, client_supplier=DefaultClientSupplier()) + + initial_materials = EncryptionMaterials( + algorithm=ALGORITHM, + encryption_context={}, + data_encryption_key=RawDataKey( + key_provider=MasterKeyInfo(provider_id="foo", key_info=b"bar"), data_key=os.urandom(ALGORITHM.kdf_input_len) + ), + ) + + result_materials = keyring.on_encrypt(initial_materials) + + assert result_materials is not initial_materials + assert result_materials.data_encryption_key is not None + assert len(result_materials.encrypted_data_keys) == 1 + + generator_flags = _matching_flags( + MasterKeyInfo(provider_id=KEY_NAMESPACE, key_info=fake_generator), result_materials.keyring_trace + ) + + assert KeyringTraceFlag.GENERATED_DATA_KEY not in generator_flags + assert KeyringTraceFlag.ENCRYPTED_DATA_KEY in generator_flags + assert KeyringTraceFlag.SIGNED_ENCRYPTION_CONTEXT in generator_flags + + +@mock_kms +def test_aws_kms_single_cmk_keyring_on_encrypt_fail(): + # In this context there are no KMS CMKs, so any calls to KMS will fail. + keyring = _AwsKmsSingleCmkKeyring(key_id="foo", client_supplier=DefaultClientSupplier()) + + initial_materials = EncryptionMaterials(algorithm=ALGORITHM, encryption_context={}) + + with pytest.raises(EncryptKeyError) as excinfo: + keyring.on_encrypt(initial_materials) + + excinfo.match(r"Unable to generate or encrypt data key using *") + + +@mock_kms +def test_aws_kms_single_cmk_keyring_on_decrypt_existing_datakey(caplog): + # In this context there are no KMS CMKs, so any calls to KMS will fail. + caplog.set_level(logging.DEBUG) + keyring = _AwsKmsSingleCmkKeyring(key_id="foo", client_supplier=DefaultClientSupplier()) + + initial_materials = DecryptionMaterials( + algorithm=ALGORITHM, + encryption_context={}, + data_encryption_key=RawDataKey( + key_provider=MasterKeyInfo(provider_id="foo", key_info=b"bar"), data_key=os.urandom(ALGORITHM.kdf_input_len) + ), + ) + + result_materials = keyring.on_decrypt( + decryption_materials=initial_materials, + encrypted_data_keys=( + EncryptedDataKey( + key_provider=MasterKeyInfo(provider_id=KEY_NAMESPACE, key_info=b"foo"), encrypted_data_key=b"bar" + ), + ), + ) + + assert result_materials.data_encryption_key == initial_materials.data_encryption_key + + log_data = caplog.text + # This means that it did NOT try to decrypt the EDK. + assert "Unable to decrypt encrypted data key from" not in log_data + + +def test_aws_kms_single_cmk_keyring_on_decrypt_single_cmk(fake_generator): + keyring = _AwsKmsSingleCmkKeyring(key_id=fake_generator, client_supplier=DefaultClientSupplier()) + + initial_encryption_materials = EncryptionMaterials(algorithm=ALGORITHM, encryption_context={}) + + encryption_materials = keyring.on_encrypt(initial_encryption_materials) + + initial_decryption_materials = DecryptionMaterials( + algorithm=encryption_materials.algorithm, encryption_context=encryption_materials.encryption_context + ) + + result_materials = keyring.on_decrypt( + decryption_materials=initial_decryption_materials, encrypted_data_keys=encryption_materials.encrypted_data_keys + ) + + assert result_materials is not initial_decryption_materials + assert result_materials.data_encryption_key is not None + + generator_flags = _matching_flags( + MasterKeyInfo(provider_id=KEY_NAMESPACE, key_info=fake_generator), result_materials.keyring_trace + ) + + assert KeyringTraceFlag.DECRYPTED_DATA_KEY in generator_flags + assert KeyringTraceFlag.VERIFIED_ENCRYPTION_CONTEXT in generator_flags + + +def test_aws_kms_single_cmk_keyring_on_decrypt_multiple_cmk(fake_generator_and_child): + generator, child = fake_generator_and_child + + encrypting_keyring = KmsKeyring(generator_key_id=generator, key_ids=(child,)) + decrypting_keyring = _AwsKmsSingleCmkKeyring(key_id=child, client_supplier=DefaultClientSupplier()) + + initial_encryption_materials = EncryptionMaterials(algorithm=ALGORITHM, encryption_context={}) + + encryption_materials = encrypting_keyring.on_encrypt(initial_encryption_materials) + + initial_decryption_materials = DecryptionMaterials( + algorithm=encryption_materials.algorithm, encryption_context=encryption_materials.encryption_context + ) + + result_materials = decrypting_keyring.on_decrypt( + decryption_materials=initial_decryption_materials, encrypted_data_keys=encryption_materials.encrypted_data_keys + ) + + generator_flags = _matching_flags( + MasterKeyInfo(provider_id=KEY_NAMESPACE, key_info=generator), result_materials.keyring_trace + ) + assert len(generator_flags) == 0 + + child_flags = _matching_flags( + MasterKeyInfo(provider_id=KEY_NAMESPACE, key_info=child), result_materials.keyring_trace + ) + + assert KeyringTraceFlag.DECRYPTED_DATA_KEY in child_flags + assert KeyringTraceFlag.VERIFIED_ENCRYPTION_CONTEXT in child_flags + + +def test_aws_kms_single_cmk_keyring_on_decrypt_no_match(fake_generator_and_child): + generator, child = fake_generator_and_child + + encrypting_keyring = _AwsKmsSingleCmkKeyring(key_id=generator, client_supplier=DefaultClientSupplier()) + decrypting_keyring = _AwsKmsSingleCmkKeyring(key_id=child, client_supplier=DefaultClientSupplier()) + + initial_encryption_materials = EncryptionMaterials(algorithm=ALGORITHM, encryption_context={}) + + encryption_materials = encrypting_keyring.on_encrypt(initial_encryption_materials) + + initial_decryption_materials = DecryptionMaterials( + algorithm=encryption_materials.algorithm, encryption_context=encryption_materials.encryption_context + ) + + result_materials = decrypting_keyring.on_decrypt( + decryption_materials=initial_decryption_materials, encrypted_data_keys=encryption_materials.encrypted_data_keys + ) + + assert result_materials.data_encryption_key is None + + +@mock_kms +def test_aws_kms_single_cmk_keyring_on_decrypt_fail(caplog): + # In this context there are no KMS CMKs, so any calls to KMS will fail. + caplog.set_level(logging.DEBUG) + keyring = _AwsKmsSingleCmkKeyring(key_id="foo", client_supplier=DefaultClientSupplier()) + + initial_materials = DecryptionMaterials(algorithm=ALGORITHM, encryption_context={}) + + result_materials = keyring.on_decrypt( + decryption_materials=initial_materials, + encrypted_data_keys=( + EncryptedDataKey( + key_provider=MasterKeyInfo(provider_id=KEY_NAMESPACE, key_info=b"foo"), encrypted_data_key=b"bar" + ), + ), + ) + + assert not result_materials.data_encryption_key + + log_data = caplog.text + + # This means that it did actually try to decrypt the EDK but encountered an error talking to KMS. + assert "Unable to decrypt encrypted data key from" in log_data + + +def test_aws_kms_discovery_keyring_on_encrypt(): + keyring = _AwsKmsDiscoveryKeyring(client_supplier=DefaultClientSupplier()) + + initial_materials = EncryptionMaterials(algorithm=ALGORITHM, encryption_context={}) + + result_materials = keyring.on_encrypt(initial_materials) + + assert result_materials is initial_materials + assert len(result_materials.encrypted_data_keys) == 0 + + +@pytest.fixture +def encryption_materials_for_discovery_decrypt(fake_generator): + encrypting_keyring = _AwsKmsSingleCmkKeyring(key_id=fake_generator, client_supplier=DefaultClientSupplier()) + + initial_encryption_materials = EncryptionMaterials(algorithm=ALGORITHM, encryption_context={}) + + return fake_generator, encrypting_keyring.on_encrypt(initial_encryption_materials) + + +def test_aws_kms_discovery_keyring_on_decrypt(encryption_materials_for_discovery_decrypt): + generator_key_id, encryption_materials = encryption_materials_for_discovery_decrypt + + decrypting_keyring = _AwsKmsDiscoveryKeyring(client_supplier=DefaultClientSupplier()) + + initial_decryption_materials = DecryptionMaterials( + algorithm=encryption_materials.algorithm, encryption_context=encryption_materials.encryption_context + ) + + result_materials = decrypting_keyring.on_decrypt( + decryption_materials=initial_decryption_materials, encrypted_data_keys=encryption_materials.encrypted_data_keys + ) + + assert result_materials is not initial_decryption_materials + assert result_materials.data_encryption_key is not None + + generator_flags = _matching_flags( + MasterKeyInfo(provider_id=KEY_NAMESPACE, key_info=generator_key_id), result_materials.keyring_trace + ) + + assert KeyringTraceFlag.DECRYPTED_DATA_KEY in generator_flags + assert KeyringTraceFlag.VERIFIED_ENCRYPTION_CONTEXT in generator_flags + + +@mock_kms +def test_aws_kms_discovery_keyring_on_decrypt_existing_data_key(caplog): + # In this context there are no KMS CMKs, so any calls to KMS will fail. + caplog.set_level(logging.DEBUG) + keyring = _AwsKmsDiscoveryKeyring(client_supplier=DefaultClientSupplier()) + + initial_materials = DecryptionMaterials( + algorithm=ALGORITHM, + encryption_context={}, + data_encryption_key=RawDataKey( + key_provider=MasterKeyInfo(provider_id="foo", key_info=b"bar"), data_key=os.urandom(ALGORITHM.kdf_input_len) + ), + ) + + result_materials = keyring.on_decrypt( + decryption_materials=initial_materials, + encrypted_data_keys=( + EncryptedDataKey( + key_provider=MasterKeyInfo(provider_id=KEY_NAMESPACE, key_info=b"foo"), encrypted_data_key=b"bar" + ), + ), + ) + + assert result_materials.data_encryption_key == initial_materials.data_encryption_key + + log_data = caplog.text + # This means that it did NOT try to decrypt the EDK. + assert "Unable to decrypt encrypted data key from" not in log_data + + +@mock_kms +def test_aws_kms_discovery_keyring_on_decrypt_no_matching_edk(caplog): + # In this context there are no KMS CMKs, so any calls to KMS will fail. + caplog.set_level(logging.DEBUG) + keyring = _AwsKmsDiscoveryKeyring(client_supplier=DefaultClientSupplier()) + + initial_materials = DecryptionMaterials(algorithm=ALGORITHM, encryption_context={},) + + result_materials = keyring.on_decrypt( + decryption_materials=initial_materials, + encrypted_data_keys=( + EncryptedDataKey(key_provider=MasterKeyInfo(provider_id="foo", key_info=b"bar"), encrypted_data_key=b"bar"), + ), + ) + + assert result_materials.data_encryption_key is None + + log_data = caplog.text + # This means that it did NOT try to decrypt the EDK. + assert "Unable to decrypt encrypted data key from" not in log_data + + +@mock_kms +def test_aws_kms_discovery_keyring_on_decrypt_fail(caplog): + # In this context there are no KMS CMKs, so any calls to KMS will fail. + caplog.set_level(logging.DEBUG) + keyring = _AwsKmsDiscoveryKeyring(client_supplier=DefaultClientSupplier()) + + initial_materials = DecryptionMaterials(algorithm=ALGORITHM, encryption_context={},) + + result_materials = keyring.on_decrypt( + decryption_materials=initial_materials, + encrypted_data_keys=( + EncryptedDataKey( + key_provider=MasterKeyInfo(provider_id=KEY_NAMESPACE, key_info=b"bar"), encrypted_data_key=b"bar" + ), + ), + ) + + assert result_materials.data_encryption_key is None + + log_data = caplog.text + # This means that it did actually try to decrypt the EDK but encountered an error talking to KMS. + assert "Unable to decrypt encrypted data key from" in log_data + + +def test_try_aws_kms_decrypt_succeed(fake_generator): + encryption_context = {"foo": "bar"} + kms = boto3.client("kms", region_name=FAKE_REGION) + plaintext = b"0123" * 8 + response = kms.encrypt(KeyId=fake_generator, Plaintext=plaintext, EncryptionContext=encryption_context) + + encrypted_data_key = EncryptedDataKey( + key_provider=MasterKeyInfo(provider_id=KEY_NAMESPACE, key_info=response["KeyId"]), + encrypted_data_key=response["CiphertextBlob"], + ) + + initial_decryption_materials = DecryptionMaterials(algorithm=ALGORITHM, encryption_context=encryption_context,) + + result_materials = _try_aws_kms_decrypt( + client_supplier=DefaultClientSupplier(), + decryption_materials=initial_decryption_materials, + grant_tokens=[], + encrypted_data_key=encrypted_data_key, + ) + + assert result_materials.data_encryption_key.data_key == plaintext + + generator_flags = _matching_flags( + MasterKeyInfo(provider_id=KEY_NAMESPACE, key_info=fake_generator), result_materials.keyring_trace + ) + + assert KeyringTraceFlag.DECRYPTED_DATA_KEY in generator_flags + assert KeyringTraceFlag.VERIFIED_ENCRYPTION_CONTEXT in generator_flags + + +@mock_kms +def test_try_aws_kms_decrypt_error(caplog): + # In this context there are no KMS CMKs, so any calls to KMS will fail. + caplog.set_level(logging.DEBUG) + + encrypted_data_key = EncryptedDataKey( + key_provider=MasterKeyInfo(provider_id=KEY_NAMESPACE, key_info=b"foo"), encrypted_data_key=b"bar" + ) + + initial_decryption_materials = DecryptionMaterials(algorithm=ALGORITHM, encryption_context={},) + + result_materials = _try_aws_kms_decrypt( + client_supplier=DefaultClientSupplier(), + decryption_materials=initial_decryption_materials, + grant_tokens=[], + encrypted_data_key=encrypted_data_key, + ) + + assert result_materials.data_encryption_key is None + + log_data = caplog.text + # This means that it did actually try to decrypt the EDK but encountered an error talking to KMS. + assert "Unable to decrypt encrypted data key from" in log_data + + +def test_do_aws_kms_decrypt(fake_generator): + encryption_context = {"foo": "bar"} + kms = boto3.client("kms", region_name=FAKE_REGION) + plaintext = b"0123" * 8 + response = kms.encrypt(KeyId=fake_generator, Plaintext=plaintext, EncryptionContext=encryption_context) + + encrypted_data_key = EncryptedDataKey( + key_provider=MasterKeyInfo(provider_id=KEY_NAMESPACE, key_info=response["KeyId"]), + encrypted_data_key=response["CiphertextBlob"], + ) + + decrypted_data_key = _do_aws_kms_decrypt( + client_supplier=DefaultClientSupplier(), + key_name=fake_generator, + encrypted_data_key=encrypted_data_key, + encryption_context=encryption_context, + grant_tokens=[], + ) + assert decrypted_data_key.data_key == plaintext + + +def test_do_aws_kms_decrypt_unexpected_key_id(fake_generator_and_child): + encryptor, decryptor = fake_generator_and_child + encryption_context = {"foo": "bar"} + kms = boto3.client("kms", region_name=FAKE_REGION) + plaintext = b"0123" * 8 + response = kms.encrypt(KeyId=encryptor, Plaintext=plaintext, EncryptionContext=encryption_context) + + encrypted_data_key = EncryptedDataKey( + key_provider=MasterKeyInfo(provider_id=KEY_NAMESPACE, key_info=response["KeyId"]), + encrypted_data_key=response["CiphertextBlob"], + ) + + with pytest.raises(DecryptKeyError) as excinfo: + _do_aws_kms_decrypt( + client_supplier=DefaultClientSupplier(), + key_name=decryptor, + encrypted_data_key=encrypted_data_key, + encryption_context=encryption_context, + grant_tokens=[], + ) + + excinfo.match(r"Decryption results from AWS KMS are for an unexpected key ID*") + + +def test_do_aws_kms_encrypt(fake_generator): + encryption_context = {"foo": "bar"} + plaintext = b"0123" * 8 + + encrypted_key = _do_aws_kms_encrypt( + client_supplier=DefaultClientSupplier(), + key_name=fake_generator, + plaintext_data_key=RawDataKey( + key_provider=MasterKeyInfo(provider_id=KEY_NAMESPACE, key_info=fake_generator), data_key=plaintext + ), + encryption_context=encryption_context, + grant_tokens=[], + ) + + kms = boto3.client("kms", region_name=FAKE_REGION) + response = kms.decrypt(CiphertextBlob=encrypted_key.encrypted_data_key, EncryptionContext=encryption_context) + + assert response["Plaintext"] == plaintext + + +def test_do_aws_kms_generate_data_key(fake_generator): + encryption_context = {"foo": "bar"} + plaintext_key, encrypted_key = _do_aws_kms_generate_data_key( + client_supplier=DefaultClientSupplier(), + key_name=fake_generator, + encryption_context=encryption_context, + algorithm=ALGORITHM, + grant_tokens=[], + ) + + kms = boto3.client("kms", region_name=FAKE_REGION) + response = kms.decrypt(CiphertextBlob=encrypted_key.encrypted_data_key, EncryptionContext=encryption_context) + + assert response["Plaintext"] == plaintext_key.data_key diff --git a/test/functional/keyrings/aws_kms/test_client_cache.py b/test/functional/keyrings/aws_kms/test_client_cache.py new file mode 100644 index 000000000..06c6c51c0 --- /dev/null +++ b/test/functional/keyrings/aws_kms/test_client_cache.py @@ -0,0 +1,33 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Functional tests for ``aws_encryption_sdk.keyrings.aws_kms.client_cache``.""" +import pytest +from botocore.config import Config +from botocore.session import Session + +from aws_encryption_sdk.keyrings.aws_kms._client_cache import ClientCache + +pytestmark = [pytest.mark.functional, pytest.mark.local] + + +def test_client_cache_caches_clients(): + cache = ClientCache(botocore_session=Session(), client_config=Config()) + + initial_client = cache.client("us-west-2", "kms") + + test = cache.client("us-west-2", "kms") + + assert "us-west-2" in cache._cache + assert test is initial_client + + +def test_client_cache_new_client(): + cache = ClientCache(botocore_session=Session(), client_config=Config()) + + initial_client = cache.client("us-west-2", "kms") + + cache._cache.pop("us-west-2") + + test = cache.client("us-west-2", "kms") + + assert test is not initial_client diff --git a/test/functional/keyrings/aws_kms/test_client_suppliers.py b/test/functional/keyrings/aws_kms/test_client_suppliers.py new file mode 100644 index 000000000..2d63fcc02 --- /dev/null +++ b/test/functional/keyrings/aws_kms/test_client_suppliers.py @@ -0,0 +1,116 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Functional tests for ``aws_encryption_sdk.keyrings.aws_kms.client_suppliers``.""" +import pytest +from botocore.config import Config +from botocore.session import Session + +from aws_encryption_sdk.exceptions import UnknownRegionError +from aws_encryption_sdk.keyrings.aws_kms.client_suppliers import ( + AllowRegionsClientSupplier, + ClientSupplier, + DefaultClientSupplier, + DenyRegionsClientSupplier, +) + +pytestmark = [pytest.mark.functional, pytest.mark.local] + + +def test_default_supplier_not_implemented(): + test = ClientSupplier() + + with pytest.raises(NotImplementedError) as excinfo: + test("region") + + excinfo.match("'ClientSupplier' is not callable") + + +def test_default_supplier_uses_cache(): + supplier = DefaultClientSupplier() + + region = "us-west-2" + expected = supplier._client_cache.client(region_name=region, service="kms") + + test = supplier(region) + + assert test is expected + + +def test_default_supplier_passes_through_configs(): + session = Session() + config = Config() + + test = DefaultClientSupplier(botocore_session=session, client_config=config) + + assert test._client_cache._botocore_session is session + assert test._client_cache._client_config is config + + +@pytest.mark.parametrize( + "kwargs", + ( + pytest.param(dict(allowed_regions="foo"), id="allowed_regions is a string"), + pytest.param(dict(allowed_regions=["foo", 5]), id="allowed_regions contains invalid type"), + ), +) +def test_allow_regions_supplier_invalid_parameters(kwargs): + with pytest.raises(TypeError): + AllowRegionsClientSupplier(**kwargs) + + +def test_allow_regions_supplier_allows_allowed_region(): + test = AllowRegionsClientSupplier(allowed_regions=["us-west-2", "us-east-2"]) + + assert test("us-west-2") + + +def test_allow_regions_supplier_denied_not_allowed_region(): + test = AllowRegionsClientSupplier(allowed_regions=["us-west-2", "us-east-2"]) + + with pytest.raises(UnknownRegionError) as excinfo: + test("ap-northeast-2") + + excinfo.match("Unable to provide client for region 'ap-northeast-2'") + + +@pytest.mark.parametrize( + "kwargs", + ( + pytest.param(dict(denied_regions="foo"), id="denied_regions is a string"), + pytest.param(dict(denied_regions=["foo", 5]), id="denied_regions contains invalid type"), + ), +) +def test_deny_regions_supplier_invalid_parameters(kwargs): + with pytest.raises(TypeError): + DenyRegionsClientSupplier(**kwargs) + + +def test_deny_regions_supplier_denies_denied_region(): + test = DenyRegionsClientSupplier(denied_regions=["us-west-2", "us-east-2"]) + + with pytest.raises(UnknownRegionError) as excinfo: + test("us-west-2") + + excinfo.match("Unable to provide client for region 'us-west-2'") + + +def test_deny_regions_supplier_allows_not_denied_region(): + test = DenyRegionsClientSupplier(denied_regions=["us-west-2", "us-east-2"]) + + assert test("ap-northeast-2") + + +def test_allow_deny_nested_supplier(): + test_allow = AllowRegionsClientSupplier( + allowed_regions=["us-west-2", "us-east-2"], client_supplier=DefaultClientSupplier() + ) + test_deny = DenyRegionsClientSupplier(denied_regions=["us-west-2"], client_supplier=test_allow) + + # test_allow allows us-west-2 + test_allow("us-west-2") + + # test_deny denies us-west-2 even though its internal supplier (test_allow) allows it + with pytest.raises(UnknownRegionError) as excinfo: + test_deny("us-west-2") + + excinfo.match("Unable to provide client for region 'us-west-2'") diff --git a/test/functional/keyrings/raw/__init__.py b/test/functional/keyrings/raw/__init__.py new file mode 100644 index 000000000..ad0e71d6c --- /dev/null +++ b/test/functional/keyrings/raw/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Dummy stub to make linters work better.""" diff --git a/test/functional/keyrings/raw/test_raw_aes.py b/test/functional/keyrings/raw/test_raw_aes.py new file mode 100644 index 000000000..9759f2ce9 --- /dev/null +++ b/test/functional/keyrings/raw/test_raw_aes.py @@ -0,0 +1,181 @@ +# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Functional tests for Raw AES keyring encryption decryption path.""" + +import pytest + +from aws_encryption_sdk.identifiers import ( + Algorithm, + EncryptionKeyType, + EncryptionType, + KeyringTraceFlag, + WrappingAlgorithm, +) +from aws_encryption_sdk.internal.crypto import WrappingKey +from aws_encryption_sdk.internal.formatting.serialize import serialize_raw_master_key_prefix +from aws_encryption_sdk.key_providers.raw import RawMasterKey +from aws_encryption_sdk.keyrings.raw import RawAESKeyring +from aws_encryption_sdk.materials_managers import DecryptionMaterials, EncryptionMaterials +from aws_encryption_sdk.structures import KeyringTrace, MasterKeyInfo, RawDataKey + +pytestmark = [pytest.mark.functional, pytest.mark.local] + +_ENCRYPTION_CONTEXT = {"encryption": "context", "values": "here"} +_PROVIDER_ID = "Random Raw Keys" +_KEY_ID = b"5325b043-5843-4629-869c-64794af77ada" +_WRAPPING_KEY = b"12345678901234567890123456789012" +_SIGNING_KEY = b"aws-crypto-public-key" + +_WRAPPING_ALGORITHM = [alg for alg in WrappingAlgorithm if alg.encryption_type is EncryptionType.SYMMETRIC] + + +def sample_encryption_materials(): + return [ + EncryptionMaterials( + algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + encryption_context=_ENCRYPTION_CONTEXT, + signing_key=_SIGNING_KEY, + ), + EncryptionMaterials( + algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + data_encryption_key=RawDataKey( + key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID), + data_key=b'*!\xa1"^-(\xf3\x105\x05i@B\xc2\xa2\xb7\xdd\xd5\xd5\xa9\xddm\xfae\xa8\\$\xf9d\x1e(', + ), + encryption_context=_ENCRYPTION_CONTEXT, + signing_key=_SIGNING_KEY, + keyring_trace=[ + KeyringTrace( + wrapping_key=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID), + flags={KeyringTraceFlag.GENERATED_DATA_KEY}, + ) + ], + ), + ] + + +@pytest.mark.parametrize("encryption_materials_samples", sample_encryption_materials()) +def test_raw_aes_encryption_decryption(encryption_materials_samples): + + # Initializing attributes + key_namespace = _PROVIDER_ID + key_name = _KEY_ID + + # Creating an instance of a raw AES keyring + test_raw_aes_keyring = RawAESKeyring(key_namespace=key_namespace, key_name=key_name, wrapping_key=_WRAPPING_KEY,) + + # Call on_encrypt function for the keyring + encryption_materials = test_raw_aes_keyring.on_encrypt(encryption_materials=encryption_materials_samples) + + # Generate decryption materials + decryption_materials = DecryptionMaterials( + algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + verification_key=b"ex_verification_key", + encryption_context=_ENCRYPTION_CONTEXT, + ) + + # Call on_decrypt function for the keyring + decryption_materials = test_raw_aes_keyring.on_decrypt( + decryption_materials=decryption_materials, encrypted_data_keys=encryption_materials.encrypted_data_keys + ) + + # Check if the data keys match + assert encryption_materials.data_encryption_key.data_key == decryption_materials.data_encryption_key.data_key + + +@pytest.mark.parametrize("encryption_materials_samples", sample_encryption_materials()) +def test_raw_master_key_decrypts_what_raw_keyring_encrypts(encryption_materials_samples): + + # Initializing attributes + key_namespace = _PROVIDER_ID + key_name = _KEY_ID + + # Creating an instance of a raw AES keyring + test_raw_aes_keyring = RawAESKeyring(key_namespace=key_namespace, key_name=key_name, wrapping_key=_WRAPPING_KEY,) + + # Creating an instance of a raw master key + test_raw_master_key = RawMasterKey( + key_id=test_raw_aes_keyring.key_name, + provider_id=test_raw_aes_keyring.key_namespace, + wrapping_key=test_raw_aes_keyring._wrapping_key_structure, + ) + + # Encrypt using raw AES keyring + encryption_materials = test_raw_aes_keyring.on_encrypt(encryption_materials=encryption_materials_samples) + + # Check if plaintext data key encrypted by raw keyring is decrypted by raw master key + + raw_mkp_decrypted_data_key = test_raw_master_key.decrypt_data_key_from_list( + encrypted_data_keys=encryption_materials._encrypted_data_keys, + algorithm=encryption_materials.algorithm, + encryption_context=encryption_materials.encryption_context, + ).data_key + + assert encryption_materials.data_encryption_key.data_key == raw_mkp_decrypted_data_key + + +@pytest.mark.parametrize("encryption_materials_samples", sample_encryption_materials()) +def test_raw_keyring_decrypts_what_raw_master_key_encrypts(encryption_materials_samples): + + # Initializing attributes + key_namespace = _PROVIDER_ID + key_name = _KEY_ID + + # Creating an instance of a raw AES keyring + test_raw_aes_keyring = RawAESKeyring(key_namespace=key_namespace, key_name=key_name, wrapping_key=_WRAPPING_KEY,) + + # Creating an instance of a raw master key + test_raw_master_key = RawMasterKey( + key_id=test_raw_aes_keyring.key_name, + provider_id=test_raw_aes_keyring.key_namespace, + wrapping_key=test_raw_aes_keyring._wrapping_key_structure, + ) + + if encryption_materials_samples.data_encryption_key is None: + return + raw_master_key_encrypted_data_key = test_raw_master_key.encrypt_data_key( + data_key=encryption_materials_samples.data_encryption_key, + algorithm=encryption_materials_samples.algorithm, + encryption_context=encryption_materials_samples.encryption_context, + ) + + # Check if plaintext data key encrypted by raw master key is decrypted by raw keyring + + raw_aes_keyring_decrypted_data_key = test_raw_aes_keyring.on_decrypt( + decryption_materials=DecryptionMaterials( + algorithm=encryption_materials_samples.algorithm, + encryption_context=encryption_materials_samples.encryption_context, + verification_key=b"ex_verification_key", + ), + encrypted_data_keys=[raw_master_key_encrypted_data_key], + ).data_encryption_key.data_key + + assert encryption_materials_samples.data_encryption_key.data_key == raw_aes_keyring_decrypted_data_key + + +@pytest.mark.parametrize("wrapping_algorithm", _WRAPPING_ALGORITHM) +def test_key_info_prefix_vectors(wrapping_algorithm): + assert ( + serialize_raw_master_key_prefix( + raw_master_key=RawMasterKey( + provider_id=_PROVIDER_ID, + key_id=_KEY_ID, + wrapping_key=WrappingKey( + wrapping_algorithm=wrapping_algorithm, + wrapping_key=_WRAPPING_KEY, + wrapping_key_type=EncryptionKeyType.SYMMETRIC, + ), + ) + ) + == _KEY_ID + b"\x00\x00\x00\x80\x00\x00\x00\x0c" + ) diff --git a/test/functional/keyrings/raw/test_raw_rsa.py b/test/functional/keyrings/raw/test_raw_rsa.py new file mode 100644 index 000000000..b7a278d2b --- /dev/null +++ b/test/functional/keyrings/raw/test_raw_rsa.py @@ -0,0 +1,297 @@ +# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Functional tests for Raw AES keyring encryption decryption path.""" + +import pytest +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import rsa + +from aws_encryption_sdk.identifiers import ( + Algorithm, + EncryptionKeyType, + EncryptionType, + KeyringTraceFlag, + WrappingAlgorithm, +) +from aws_encryption_sdk.internal.crypto import WrappingKey +from aws_encryption_sdk.key_providers.raw import RawMasterKey +from aws_encryption_sdk.keyrings.raw import RawRSAKeyring +from aws_encryption_sdk.materials_managers import DecryptionMaterials, EncryptionMaterials +from aws_encryption_sdk.structures import KeyringTrace, MasterKeyInfo, RawDataKey + +pytestmark = [pytest.mark.functional, pytest.mark.local] + +_ENCRYPTION_CONTEXT = {"encryption": "context", "values": "here"} +_PROVIDER_ID = "Random Raw Keys" +_KEY_ID = b"5325b043-5843-4629-869c-64794af77ada" +_WRAPPING_ALGORITHM = WrappingAlgorithm.RSA_OAEP_SHA256_MGF1 + +_PUBLIC_EXPONENT = 65537 +_KEY_SIZE = 2048 +_BACKEND = default_backend() + +_PRIVATE_WRAPPING_KEY = rsa.generate_private_key(public_exponent=_PUBLIC_EXPONENT, key_size=_KEY_SIZE, backend=_BACKEND) + +_PRIVATE_WRAPPING_KEY_PEM = ( + b"-----BEGIN RSA PRIVATE KEY-----\n" + b"MIIEowIBAAKCAQEAo8uCyhiO4JUGZV+rtNq5DBA9Lm4xkw5kTA3v6EPybs8bVXL2\n" + b"ZE6jkbo+xT4Jg/bKzUpnp1fE+T1ruGPtsPdoEmhY/P64LDNIs3sRq5U4QV9IETU1\n" + b"vIcbNNkgGhRjV8J87YNY0tV0H7tuWuZRpqnS+gjV6V9lUMkbvjMCc5IBqQc3heut\n" + b"/+fH4JwpGlGxOVXI8QAapnSy1XpCr3+PT29kydVJnIMuAoFrurojRpOQbOuVvhtA\n" + b"gARhst1Ji4nfROGYkj6eZhvkz2Bkud4/+3lGvVU5LO1vD8oY7WoGtpin3h50VcWe\n" + b"aBT4kejx4s9/G9C4R24lTH09J9HO2UUsuCqZYQIDAQABAoIBAQCfC90bCk+qaWqF\n" + b"gymC+qOWwCn4bM28gswHQb1D5r6AtKBRD8mKywVvWs7azguFVV3Fi8sspkBA2FBC\n" + b"At5p6ULoJOTL/TauzLl6djVJTCMM701WUDm2r+ZOIctXJ5bzP4n5Q4I7b0NMEL7u\n" + b"ixib4elYGr5D1vrVQAKtZHCr8gmkqyx8Mz7wkJepzBP9EeVzETCHsmiQDd5WYlO1\n" + b"C2IQYgw6MJzgM4entJ0V/GPytkodblGY95ORVK7ZhyNtda+r5BZ6/jeMW+hA3VoK\n" + b"tHSWjHt06ueVCCieZIATmYzBNt+zEz5UA2l7ksg3eWfVORJQS7a6Ef4VvbJLM9Ca\n" + b"m1kdsjelAoGBANKgvRf39i3bSuvm5VoyJuqinSb/23IH3Zo7XOZ5G164vh49E9Cq\n" + b"dOXXVxox74ppj/kbGUoOk+AvaB48zzfzNvac0a7lRHExykPH2kVrI/NwH/1OcT/x\n" + b"2e2DnFYocXcb4gbdZQ+m6X3zkxOYcONRzPVW1uMrFTWHcJveMUm4PGx7AoGBAMcU\n" + b"IRvrT6ye5se0s27gHnPweV+3xjsNtXZcK82N7duXyHmNjxrwOAv0SOhUmTkRXArM\n" + b"6aN5D8vyZBSWma2TgUKwpQYFTI+4Sp7sdkkyojGAEixJ+c5TZJNxZFrUe0FwAoic\n" + b"c2kb7ntaiEj5G+qHvykJJro5hy6uLnjiMVbAiJDTAoGAKb67241EmHAXGEwp9sdr\n" + b"2SMjnIAnQSF39UKAthkYqJxa6elXDQtLoeYdGE7/V+J2K3wIdhoPiuY6b4vD0iX9\n" + b"JcGM+WntN7YTjX2FsC588JmvbWfnoDHR7HYiPR1E58N597xXdFOzgUgORVr4PMWQ\n" + b"pqtwaZO3X2WZlvrhr+e46hMCgYBfdIdrm6jYXFjL6RkgUNZJQUTxYGzsY+ZemlNm\n" + b"fGdQo7a8kePMRuKY2MkcnXPaqTg49YgRmjq4z8CtHokRcWjJUWnPOTs8rmEZUshk\n" + b"0KJ0mbQdCFt/Uv0mtXgpFTkEZ3DPkDTGcV4oR4CRfOCl0/EU/A5VvL/U4i/mRo7h\n" + b"ye+xgQKBgD58b+9z+PR5LAJm1tZHIwb4tnyczP28PzwknxFd2qylR4ZNgvAUqGtU\n" + b"xvpUDpzMioz6zUH9YV43YNtt+5Xnzkqj+u9Mr27/H2v9XPwORGfwQ5XPwRJz/2oC\n" + b"EnPmP1SZoY9lXKUpQXHXSpDZ2rE2Klt3RHMUMHt8Zpy36E8Vwx8o\n" + b"-----END RSA PRIVATE KEY-----\n" +) + +_RAW_RSA_PRIVATE_KEY_PEM_ENCODED_WITHOUT_PASSWORD = rsa.generate_private_key( + public_exponent=_PUBLIC_EXPONENT, key_size=_KEY_SIZE, backend=_BACKEND +).private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption(), +) + +_RAW_RSA_PRIVATE_KEY_PEM_ENCODED_WITH_PASSWORD = rsa.generate_private_key( + public_exponent=_PUBLIC_EXPONENT, key_size=_KEY_SIZE, backend=_BACKEND +).private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.BestAvailableEncryption(b"mypassword"), +) + +_RAW_RSA_PUBLIC_KEY_PEM_ENCODED = ( + rsa.generate_private_key(public_exponent=_PUBLIC_EXPONENT, key_size=_KEY_SIZE, backend=_BACKEND) + .public_key() + .public_bytes(encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo) +) + +_RAW_RSA_PRIVATE_KEY_DER_ENCODED_WITHOUT_PASSWORD = rsa.generate_private_key( + public_exponent=_PUBLIC_EXPONENT, key_size=_KEY_SIZE, backend=_BACKEND +).private_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption(), +) + +_RAW_RSA_PRIVATE_KEY_DER_ENCODED_WITH_PASSWORD = rsa.generate_private_key( + public_exponent=_PUBLIC_EXPONENT, key_size=_KEY_SIZE, backend=_BACKEND +).private_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.BestAvailableEncryption(b"mypassword"), +) + +_RAW_RSA_PUBLIC_KEY_DER_ENCODED = ( + rsa.generate_private_key(public_exponent=_PUBLIC_EXPONENT, key_size=_KEY_SIZE, backend=_BACKEND) + .public_key() + .public_bytes(encoding=serialization.Encoding.DER, format=serialization.PublicFormat.SubjectPublicKeyInfo) +) + + +def sample_encryption_materials(): + return [ + EncryptionMaterials( + algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, encryption_context=_ENCRYPTION_CONTEXT + ), + EncryptionMaterials( + algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + data_encryption_key=RawDataKey( + key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID), + data_key=b'*!\xa1"^-(\xf3\x105\x05i@B\xc2\xa2\xb7\xdd\xd5\xd5\xa9\xddm\xfae\xa8\\$\xf9d\x1e(', + ), + encryption_context=_ENCRYPTION_CONTEXT, + keyring_trace=[ + KeyringTrace( + wrapping_key=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID), + flags={KeyringTraceFlag.GENERATED_DATA_KEY}, + ) + ], + ), + ] + + +def sample_raw_rsa_keyring_using_different_wrapping_algorithm(): + for alg in WrappingAlgorithm: + if alg.encryption_type is EncryptionType.ASYMMETRIC: + yield RawRSAKeyring( + key_namespace=_PROVIDER_ID, + key_name=_KEY_ID, + wrapping_algorithm=alg, + private_wrapping_key=_PRIVATE_WRAPPING_KEY, + ) + pem_and_der_encoded_raw_rsa_keyring = [ + RawRSAKeyring.from_pem_encoding( + key_namespace=_PROVIDER_ID, + key_name=_KEY_ID, + private_encoded_key=_RAW_RSA_PRIVATE_KEY_PEM_ENCODED_WITHOUT_PASSWORD, + wrapping_algorithm=_WRAPPING_ALGORITHM, + ), + RawRSAKeyring.from_pem_encoding( + key_namespace=_PROVIDER_ID, + key_name=_KEY_ID, + private_encoded_key=_RAW_RSA_PRIVATE_KEY_PEM_ENCODED_WITH_PASSWORD, + password=b"mypassword", + wrapping_algorithm=_WRAPPING_ALGORITHM, + ), + RawRSAKeyring.from_pem_encoding( + key_namespace=_PROVIDER_ID, + key_name=_KEY_ID, + public_encoded_key=_RAW_RSA_PUBLIC_KEY_PEM_ENCODED, + wrapping_algorithm=_WRAPPING_ALGORITHM, + ), + RawRSAKeyring.from_der_encoding( + key_namespace=_PROVIDER_ID, + key_name=_KEY_ID, + private_encoded_key=_RAW_RSA_PRIVATE_KEY_DER_ENCODED_WITHOUT_PASSWORD, + wrapping_algorithm=_WRAPPING_ALGORITHM, + ), + RawRSAKeyring.from_der_encoding( + key_namespace=_PROVIDER_ID, + key_name=_KEY_ID, + private_encoded_key=_RAW_RSA_PRIVATE_KEY_DER_ENCODED_WITH_PASSWORD, + password=b"mypassword", + wrapping_algorithm=_WRAPPING_ALGORITHM, + ), + RawRSAKeyring.from_der_encoding( + key_namespace=_PROVIDER_ID, + key_name=_KEY_ID, + public_encoded_key=_RAW_RSA_PUBLIC_KEY_DER_ENCODED, + password=b"mypassword", + wrapping_algorithm=_WRAPPING_ALGORITHM, + ), + ] + for keyring in pem_and_der_encoded_raw_rsa_keyring: + yield keyring + + +@pytest.mark.parametrize("encryption_materials_samples", sample_encryption_materials()) +@pytest.mark.parametrize("test_raw_rsa_keyring", sample_raw_rsa_keyring_using_different_wrapping_algorithm()) +def test_raw_rsa_encryption_decryption(encryption_materials_samples, test_raw_rsa_keyring): + + # Call on_encrypt function for the keyring + encryption_materials = test_raw_rsa_keyring.on_encrypt(encryption_materials=encryption_materials_samples) + + assert encryption_materials.encrypted_data_keys is not None + + # Generate decryption materials + decryption_materials = DecryptionMaterials( + algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + verification_key=b"ex_verification_key", + encryption_context=_ENCRYPTION_CONTEXT, + ) + + # Call on_decrypt function for the keyring + decryption_materials = test_raw_rsa_keyring.on_decrypt( + decryption_materials=decryption_materials, encrypted_data_keys=encryption_materials.encrypted_data_keys + ) + + if test_raw_rsa_keyring._private_wrapping_key is not None: + # Check if the data keys match + assert encryption_materials.data_encryption_key.data_key == decryption_materials.data_encryption_key.data_key + + +@pytest.mark.parametrize("encryption_materials_samples", sample_encryption_materials()) +def test_raw_master_key_decrypts_what_raw_keyring_encrypts(encryption_materials_samples): + test_raw_rsa_keyring = RawRSAKeyring.from_pem_encoding( + key_namespace=_PROVIDER_ID, + key_name=_KEY_ID, + wrapping_algorithm=_WRAPPING_ALGORITHM, + private_encoded_key=_PRIVATE_WRAPPING_KEY_PEM, + ) + + # Creating an instance of a raw master key + test_raw_master_key = RawMasterKey( + key_id=_KEY_ID, + provider_id=_PROVIDER_ID, + wrapping_key=WrappingKey( + wrapping_algorithm=_WRAPPING_ALGORITHM, + wrapping_key=_PRIVATE_WRAPPING_KEY_PEM, + wrapping_key_type=EncryptionKeyType.PRIVATE, + ), + ) + + # Call on_encrypt function for the keyring + encryption_materials = test_raw_rsa_keyring.on_encrypt(encryption_materials=encryption_materials_samples) + + # Check if plaintext data key encrypted by raw keyring is decrypted by raw master key + raw_mkp_decrypted_data_key = test_raw_master_key.decrypt_data_key_from_list( + encrypted_data_keys=encryption_materials._encrypted_data_keys, + algorithm=encryption_materials.algorithm, + encryption_context=encryption_materials.encryption_context, + ).data_key + + assert encryption_materials.data_encryption_key.data_key == raw_mkp_decrypted_data_key + + +@pytest.mark.parametrize("encryption_materials_samples", sample_encryption_materials()) +def test_raw_keyring_decrypts_what_raw_master_key_encrypts(encryption_materials_samples): + + # Create instance of raw master key + test_raw_master_key = RawMasterKey( + key_id=_KEY_ID, + provider_id=_PROVIDER_ID, + wrapping_key=WrappingKey( + wrapping_algorithm=_WRAPPING_ALGORITHM, + wrapping_key=_PRIVATE_WRAPPING_KEY_PEM, + wrapping_key_type=EncryptionKeyType.PRIVATE, + ), + ) + + test_raw_rsa_keyring = RawRSAKeyring.from_pem_encoding( + key_namespace=_PROVIDER_ID, + key_name=_KEY_ID, + wrapping_algorithm=_WRAPPING_ALGORITHM, + private_encoded_key=_PRIVATE_WRAPPING_KEY_PEM, + ) + + raw_mkp_generated_data_key = test_raw_master_key.generate_data_key( + algorithm=encryption_materials_samples.algorithm, + encryption_context=encryption_materials_samples.encryption_context, + ) + + raw_mkp_encrypted_data_key = test_raw_master_key.encrypt_data_key( + data_key=raw_mkp_generated_data_key, + algorithm=encryption_materials_samples.algorithm, + encryption_context=encryption_materials_samples.encryption_context, + ) + + decryption_materials = test_raw_rsa_keyring.on_decrypt( + decryption_materials=DecryptionMaterials( + algorithm=encryption_materials_samples.algorithm, + encryption_context=encryption_materials_samples.encryption_context, + verification_key=b"ex_verification_key", + ), + encrypted_data_keys=[raw_mkp_encrypted_data_key], + ) + + assert raw_mkp_generated_data_key.data_key == decryption_materials.data_encryption_key.data_key diff --git a/test/functional/keyrings/test_multi.py b/test/functional/keyrings/test_multi.py new file mode 100644 index 000000000..ff9eb2440 --- /dev/null +++ b/test/functional/keyrings/test_multi.py @@ -0,0 +1,122 @@ +# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Functional tests for Multi keyring encryption decryption path.""" + +import pytest +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.asymmetric import rsa + +from aws_encryption_sdk.identifiers import KeyringTraceFlag, WrappingAlgorithm +from aws_encryption_sdk.internal.defaults import ALGORITHM +from aws_encryption_sdk.keyrings.multi import MultiKeyring +from aws_encryption_sdk.keyrings.raw import RawAESKeyring, RawRSAKeyring +from aws_encryption_sdk.materials_managers import DecryptionMaterials, EncryptionMaterials +from aws_encryption_sdk.structures import KeyringTrace, MasterKeyInfo, RawDataKey + +pytestmark = [pytest.mark.functional, pytest.mark.local] + +_ENCRYPTION_CONTEXT = {"encryption": "context", "values": "here"} +_PROVIDER_ID = "Random Raw Keys" +_KEY_ID = b"5325b043-5843-4629-869c-64794af77ada" +_WRAPPING_KEY_AES = b"\xeby-\x80A6\x15rA8\x83#,\xe4\xab\xac`\xaf\x99Z\xc1\xce\xdb\xb6\x0f\xb7\x805\xb2\x14J3" + +_ENCRYPTION_MATERIALS_WITHOUT_DATA_KEY = EncryptionMaterials( + algorithm=ALGORITHM, encryption_context=_ENCRYPTION_CONTEXT +) + +_ENCRYPTION_MATERIALS_WITH_DATA_KEY = EncryptionMaterials( + algorithm=ALGORITHM, + data_encryption_key=RawDataKey( + key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID), + data_key=b'*!\xa1"^-(\xf3\x105\x05i@B\xc2\xa2\xb7\xdd\xd5\xd5\xa9\xddm\xfae\xa8\\$\xf9d\x1e(', + ), + encryption_context=_ENCRYPTION_CONTEXT, + keyring_trace=[ + KeyringTrace( + wrapping_key=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID), + flags={KeyringTraceFlag.GENERATED_DATA_KEY}, + ) + ], +) + +_MULTI_KEYRING_WITH_GENERATOR_AND_CHILDREN = MultiKeyring( + generator=RawAESKeyring(key_namespace=_PROVIDER_ID, key_name=_KEY_ID, wrapping_key=_WRAPPING_KEY_AES,), + children=[ + RawRSAKeyring( + key_namespace=_PROVIDER_ID, + key_name=_KEY_ID, + wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, + private_wrapping_key=rsa.generate_private_key( + public_exponent=65537, key_size=2048, backend=default_backend() + ), + ), + RawRSAKeyring( + key_namespace=_PROVIDER_ID, + key_name=_KEY_ID, + wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, + private_wrapping_key=rsa.generate_private_key( + public_exponent=65537, key_size=2048, backend=default_backend() + ), + ), + ], +) + +_MULTI_KEYRING_WITHOUT_CHILDREN = MultiKeyring( + generator=RawRSAKeyring( + key_namespace=_PROVIDER_ID, + key_name=_KEY_ID, + wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, + private_wrapping_key=rsa.generate_private_key(public_exponent=65537, key_size=2048, backend=default_backend()), + ) +) + +_MULTI_KEYRING_WITHOUT_GENERATOR = MultiKeyring( + children=[ + RawRSAKeyring( + key_namespace=_PROVIDER_ID, + key_name=_KEY_ID, + wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, + private_wrapping_key=rsa.generate_private_key( + public_exponent=65537, key_size=2048, backend=default_backend() + ), + ), + RawAESKeyring(key_namespace=_PROVIDER_ID, key_name=_KEY_ID, wrapping_key=_WRAPPING_KEY_AES,), + ] +) + + +@pytest.mark.parametrize( + "multi_keyring, encryption_materials", + [ + (_MULTI_KEYRING_WITH_GENERATOR_AND_CHILDREN, _ENCRYPTION_MATERIALS_WITHOUT_DATA_KEY), + (_MULTI_KEYRING_WITH_GENERATOR_AND_CHILDREN, _ENCRYPTION_MATERIALS_WITH_DATA_KEY), + (_MULTI_KEYRING_WITHOUT_CHILDREN, _ENCRYPTION_MATERIALS_WITH_DATA_KEY), + (_MULTI_KEYRING_WITHOUT_GENERATOR, _ENCRYPTION_MATERIALS_WITH_DATA_KEY), + ], +) +def test_multi_keyring_encryption_decryption(multi_keyring, encryption_materials): + # Call on_encrypt function for the keyring + encryption_materials = multi_keyring.on_encrypt(encryption_materials) + + # Generate decryption materials + decryption_materials = DecryptionMaterials( + algorithm=ALGORITHM, verification_key=b"ex_verification_key", encryption_context=_ENCRYPTION_CONTEXT + ) + + # Call on_decrypt function for the keyring + decryption_materials = multi_keyring.on_decrypt( + decryption_materials=decryption_materials, encrypted_data_keys=encryption_materials.encrypted_data_keys + ) + + # Check if the data keys match + assert encryption_materials.data_encryption_key == decryption_materials.data_encryption_key diff --git a/test/functional/test_f_aws_encryption_sdk_client.py b/test/functional/test_client.py similarity index 80% rename from test/functional/test_f_aws_encryption_sdk_client.py rename to test/functional/test_client.py index fb19e868a..ebe7e14d1 100644 --- a/test/functional/test_f_aws_encryption_sdk_client.py +++ b/test/functional/test_client.py @@ -1,19 +1,10 @@ -# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). You -# may not use this file except in compliance with the License. A copy of -# the License is located at -# -# http://aws.amazon.com/apache2.0/ -# -# or in the "license" file accompanying this file. This file is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF -# ANY KIND, either express or implied. See the License for the specific -# language governing permissions and limitations under the License. +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 """Functional test suite for aws_encryption_sdk.kms_thick_client""" from __future__ import division import io +import itertools import logging import attr @@ -34,10 +25,19 @@ from aws_encryption_sdk.internal.crypto.wrapping_keys import WrappingKey from aws_encryption_sdk.internal.defaults import LINE_LENGTH from aws_encryption_sdk.internal.formatting.encryption_context import serialize_encryption_context -from aws_encryption_sdk.key_providers.base import MasterKeyProviderConfig +from aws_encryption_sdk.key_providers.base import MasterKeyProvider, MasterKeyProviderConfig from aws_encryption_sdk.key_providers.raw import RawMasterKeyProvider +from aws_encryption_sdk.keyrings.base import Keyring +from aws_encryption_sdk.keyrings.raw import RawRSAKeyring from aws_encryption_sdk.materials_managers import DecryptionMaterialsRequest, EncryptionMaterialsRequest +from ..unit.unit_test_utils import ( + ephemeral_raw_aes_keyring, + ephemeral_raw_aes_master_key, + ephemeral_raw_rsa_keyring, + raw_rsa_mkps_from_keyring, +) + pytestmark = [pytest.mark.functional, pytest.mark.local] VALUES = { @@ -315,69 +315,211 @@ def test_encrypt_ciphertext_message(frame_length, algorithm, encryption_context) assert len(ciphertext) == results_length -@pytest.mark.parametrize( - "wrapping_algorithm, encryption_key_type, decryption_key_type", - ( - (WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING, EncryptionKeyType.SYMMETRIC, EncryptionKeyType.SYMMETRIC), - (WrappingAlgorithm.RSA_PKCS1, EncryptionKeyType.PRIVATE, EncryptionKeyType.PRIVATE), - (WrappingAlgorithm.RSA_PKCS1, EncryptionKeyType.PUBLIC, EncryptionKeyType.PRIVATE), - (WrappingAlgorithm.RSA_OAEP_SHA1_MGF1, EncryptionKeyType.PRIVATE, EncryptionKeyType.PRIVATE), - (WrappingAlgorithm.RSA_OAEP_SHA1_MGF1, EncryptionKeyType.PUBLIC, EncryptionKeyType.PRIVATE), - ), -) -def test_encryption_cycle_raw_mkp(caplog, wrapping_algorithm, encryption_key_type, decryption_key_type): - caplog.set_level(logging.DEBUG) +def _raw_aes(include_mkp=True): + for symmetric_algorithm in ( + WrappingAlgorithm.AES_128_GCM_IV12_TAG16_NO_PADDING, + WrappingAlgorithm.AES_192_GCM_IV12_TAG16_NO_PADDING, + WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING, + ): + keyring = ephemeral_raw_aes_keyring(symmetric_algorithm) + yield pytest.param( + "keyring", keyring, "keyring", keyring, id="raw AES keyring -- {}".format(symmetric_algorithm.name) + ) - encrypting_key_provider = build_fake_raw_key_provider(wrapping_algorithm, encryption_key_type) - decrypting_key_provider = build_fake_raw_key_provider(wrapping_algorithm, decryption_key_type) - ciphertext, _ = aws_encryption_sdk.encrypt( + if not include_mkp: + continue + + yield pytest.param( + "key_provider", + build_fake_raw_key_provider(symmetric_algorithm, EncryptionKeyType.SYMMETRIC), + "key_provider", + build_fake_raw_key_provider(symmetric_algorithm, EncryptionKeyType.SYMMETRIC), + id="raw AES master key provider -- {}".format(symmetric_algorithm.name), + ) + + mkp = ephemeral_raw_aes_master_key(wrapping_algorithm=symmetric_algorithm, key=keyring._wrapping_key) + yield pytest.param( + "key_provider", + mkp, + "keyring", + keyring, + id="raw AES -- encrypt with master key provider and decrypt with keyring -- {}".format(symmetric_algorithm), + ) + yield pytest.param( + "keyring", + keyring, + "key_provider", + mkp, + id="raw AES -- encrypt with keyring and decrypt with master key provider -- {}".format(symmetric_algorithm), + ) + + +def _raw_rsa(include_pre_sha2=True, include_sha2=True, include_mkp=True): + wrapping_algorithms = [] + if include_pre_sha2: + wrapping_algorithms.extend([WrappingAlgorithm.RSA_PKCS1, WrappingAlgorithm.RSA_OAEP_SHA1_MGF1]) + if include_sha2: + wrapping_algorithms.extend( + [ + WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, + WrappingAlgorithm.RSA_OAEP_SHA384_MGF1, + WrappingAlgorithm.RSA_OAEP_SHA512_MGF1, + ] + ) + for wrapping_algorithm in wrapping_algorithms: + private_keyring = ephemeral_raw_rsa_keyring(wrapping_algorithm=wrapping_algorithm) + public_keyring = RawRSAKeyring( + key_namespace=private_keyring.key_namespace, + key_name=private_keyring.key_name, + wrapping_algorithm=wrapping_algorithm, + public_wrapping_key=private_keyring._private_wrapping_key.public_key(), + ) + yield pytest.param( + "keyring", + private_keyring, + "keyring", + private_keyring, + id="raw RSA keyring -- private encrypt, private decrypt -- {}".format(wrapping_algorithm.name), + ) + yield pytest.param( + "keyring", + public_keyring, + "keyring", + private_keyring, + id="raw RSA keyring -- public encrypt, private decrypt -- {}".format(wrapping_algorithm.name), + ) + + if not include_mkp: + continue + + private_mkp, public_mkp = raw_rsa_mkps_from_keyring(private_keyring) + + yield pytest.param( + "key_provider", + build_fake_raw_key_provider(wrapping_algorithm, EncryptionKeyType.PRIVATE), + "key_provider", + build_fake_raw_key_provider(wrapping_algorithm, EncryptionKeyType.PRIVATE), + id="raw RSA master key provider -- private encrypt, private decrypt -- {}".format(wrapping_algorithm.name), + ) + yield pytest.param( + "key_provider", + build_fake_raw_key_provider(wrapping_algorithm, EncryptionKeyType.PUBLIC), + "key_provider", + build_fake_raw_key_provider(wrapping_algorithm, EncryptionKeyType.PRIVATE), + id="raw RSA master key provider -- public encrypt, private decrypt -- {}".format(wrapping_algorithm.name), + ) + + yield pytest.param( + "key_provider", + private_mkp, + "keyring", + private_keyring, + id="raw RSA keyring -- private master key provider encrypt and private keyring decrypt -- {}".format( + wrapping_algorithm + ), + ) + yield pytest.param( + "key_provider", + public_mkp, + "keyring", + private_keyring, + id="raw RSA keyring -- public master key provider encrypt and private keyring decrypt -- {}".format( + wrapping_algorithm + ), + ) + yield pytest.param( + "keyring", + private_keyring, + "key_provider", + private_mkp, + id="raw RSA keyring -- private keyring encrypt and private master key provider decrypt -- {}".format( + wrapping_algorithm + ), + ) + yield pytest.param( + "keyring", + public_keyring, + "key_provider", + private_mkp, + id="raw RSA keyring -- public keyring encrypt and private master key provider decrypt -- {}".format( + wrapping_algorithm + ), + ) + + +def assert_key_not_logged(provider, log_capture): + if isinstance(provider, MasterKeyProvider): + for member in provider._members: + assert repr(member.config.wrapping_key._wrapping_key)[2:-1] not in log_capture + + +def run_raw_provider_check( + log_capturer, encrypt_param_name, encrypting_provider, decrypt_param_name, decrypting_provider +): + log_capturer.set_level(logging.DEBUG) + + encrypt_kwargs = {encrypt_param_name: encrypting_provider} + decrypt_kwargs = {decrypt_param_name: decrypting_provider} + + encrypt_result = aws_encryption_sdk.encrypt( source=VALUES["plaintext_128"], - key_provider=encrypting_key_provider, encryption_context=VALUES["encryption_context"], frame_length=0, + **encrypt_kwargs ) - plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=decrypting_key_provider) + decrypt_result = aws_encryption_sdk.decrypt(source=encrypt_result.result, **decrypt_kwargs) + + if isinstance(encrypting_provider, Keyring): + trace_entries = ( + entry + for entry in encrypt_result.keyring_trace + if ( + entry.wrapping_key.provider_id == encrypting_provider.key_namespace + and entry.wrapping_key.key_info == encrypting_provider.key_name + ) + ) + assert trace_entries + + assert decrypt_result.result == VALUES["plaintext_128"] + assert_key_not_logged(encrypting_provider, log_capturer.text) + + if isinstance(decrypting_provider, Keyring): + trace_entries = ( + entry + for entry in decrypt_result.keyring_trace + if ( + entry.wrapping_key.provider_id == decrypting_provider.key_namespace + and entry.wrapping_key.key_info == decrypting_provider.key_name + ) + ) + assert trace_entries + - assert plaintext == VALUES["plaintext_128"] - for member in encrypting_key_provider._members: - assert repr(member.config.wrapping_key._wrapping_key)[2:-1] not in caplog.text +@pytest.mark.parametrize( + "encrypt_param_name, encrypting_provider, decrypt_param_name, decrypting_provider", + itertools.chain.from_iterable((_raw_aes(), _raw_rsa(include_sha2=False))), +) +def test_encryption_cycle_raw_mkp( + caplog, encrypt_param_name, encrypting_provider, decrypt_param_name, decrypting_provider +): + run_raw_provider_check(caplog, encrypt_param_name, encrypting_provider, decrypt_param_name, decrypting_provider) @pytest.mark.skipif( not _mgf1_sha256_supported(), reason="MGF1-SHA2 not supported by this backend: OpenSSL required v1.0.2+" ) @pytest.mark.parametrize( - "wrapping_algorithm", - ( - WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, - WrappingAlgorithm.RSA_OAEP_SHA384_MGF1, - WrappingAlgorithm.RSA_OAEP_SHA512_MGF1, - ), + "encrypt_param_name, encrypting_provider, decrypt_param_name, decrypting_provider", _raw_rsa(include_pre_sha2=False) ) -@pytest.mark.parametrize("encryption_key_type", (EncryptionKeyType.PUBLIC, EncryptionKeyType.PRIVATE)) -def test_encryption_cycle_raw_mkp_openssl_102_plus(wrapping_algorithm, encryption_key_type): - decryption_key_type = EncryptionKeyType.PRIVATE - encrypting_key_provider = build_fake_raw_key_provider(wrapping_algorithm, encryption_key_type) - decrypting_key_provider = build_fake_raw_key_provider(wrapping_algorithm, decryption_key_type) - ciphertext, _ = aws_encryption_sdk.encrypt( - source=VALUES["plaintext_128"], - key_provider=encrypting_key_provider, - encryption_context=VALUES["encryption_context"], - frame_length=0, - ) - plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=decrypting_key_provider) - assert plaintext == VALUES["plaintext_128"] +def test_encryption_cycle_raw_mkp_openssl_102_plus( + caplog, encrypt_param_name, encrypting_provider, decrypt_param_name, decrypting_provider +): + run_raw_provider_check(caplog, encrypt_param_name, encrypting_provider, decrypt_param_name, decrypting_provider) -@pytest.mark.parametrize( - "frame_length, algorithm, encryption_context", - [ - [frame_length, algorithm_suite, encryption_context] - for frame_length in VALUES["frame_lengths"] - for algorithm_suite in Algorithm - for encryption_context in [{}, VALUES["encryption_context"]] - ], -) +@pytest.mark.parametrize("frame_length", VALUES["frame_lengths"]) +@pytest.mark.parametrize("algorithm", Algorithm) +@pytest.mark.parametrize("encryption_context", [{}, VALUES["encryption_context"]]) def test_encryption_cycle_oneshot_kms(frame_length, algorithm, encryption_context): key_provider = fake_kms_key_provider(algorithm.kdf_input_len) @@ -394,15 +536,9 @@ def test_encryption_cycle_oneshot_kms(frame_length, algorithm, encryption_contex assert plaintext == VALUES["plaintext_128"] * 10 -@pytest.mark.parametrize( - "frame_length, algorithm, encryption_context", - [ - [frame_length, algorithm_suite, encryption_context] - for frame_length in VALUES["frame_lengths"] - for algorithm_suite in Algorithm - for encryption_context in [{}, VALUES["encryption_context"]] - ], -) +@pytest.mark.parametrize("frame_length", VALUES["frame_lengths"]) +@pytest.mark.parametrize("algorithm", Algorithm) +@pytest.mark.parametrize("encryption_context", [{}, VALUES["encryption_context"]]) def test_encryption_cycle_stream_kms(frame_length, algorithm, encryption_context): key_provider = fake_kms_key_provider(algorithm.kdf_input_len) diff --git a/test/integration/README.rst b/test/integration/README.rst index 33ecbbedd..a7dcdd5ac 100644 --- a/test/integration/README.rst +++ b/test/integration/README.rst @@ -5,8 +5,11 @@ aws-encryption-sdk Integration Tests In order to run these integration tests successfully, these things must be configured. #. Ensure that AWS credentials are available in one of the `automatically discoverable credential locations`_. -#. Set environment variable ``AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID`` to valid +#. Set environment variable ``AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID`` + and ``AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID_2`` to valid `AWS KMS key id`_ to use for integration tests. + These should be AWS KMS CMK ARNs in two different regions. + They will be used for integration tests. .. _automatically discoverable credential locations: http://boto3.readthedocs.io/en/latest/guide/configuration.html .. _AWS KMS key id: http://docs.aws.amazon.com/kms/latest/APIReference/API_Encrypt.html diff --git a/test/integration/__init__.py b/test/integration/__init__.py index 53a960891..ad0e71d6c 100644 --- a/test/integration/__init__.py +++ b/test/integration/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"). You # may not use this file except in compliance with the License. A copy of @@ -10,3 +10,4 @@ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. +"""Dummy stub to make linters work better.""" diff --git a/test/integration/integration_test_utils.py b/test/integration/integration_test_utils.py index b65d93570..3407b4f1c 100644 --- a/test/integration/integration_test_utils.py +++ b/test/integration/integration_test_utils.py @@ -14,37 +14,50 @@ import os import botocore.session +import pytest from aws_encryption_sdk.key_providers.kms import KMSMasterKeyProvider +from aws_encryption_sdk.keyrings.aws_kms import KmsKeyring AWS_KMS_KEY_ID = "AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID" +AWS_KMS_KEY_ID_2 = "AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID_2" _KMS_MKP = None _KMS_MKP_BOTO = None +_KMS_KEYRING = None -def get_cmk_arn(): - """Retrieves the target CMK ARN from environment variable.""" - arn = os.environ.get(AWS_KMS_KEY_ID, None) +def _get_single_cmk_arn(name): + # type: (str) -> str + """Retrieve a single target AWS KMS CMK ARN from the specified environment variable name.""" + arn = os.environ.get(name, None) if arn is None: raise ValueError( - 'Environment variable "{}" must be set to a valid KMS CMK ARN for integration tests to run'.format( - AWS_KMS_KEY_ID - ) + 'Environment variable "{}" must be set to a valid KMS CMK ARN for integration tests to run'.format(name) ) if arn.startswith("arn:") and ":alias/" not in arn: return arn raise ValueError("KMS CMK ARN provided for integration tests much be a key not an alias") +def get_cmk_arn(): + """Retrieves the target AWS KMS CMK ARN from environment variable.""" + return _get_single_cmk_arn(AWS_KMS_KEY_ID) + + +def get_all_cmk_arns(): + """Retrieve all known target AWS KMS CMK ARNs from environment variables.""" + return [_get_single_cmk_arn(AWS_KMS_KEY_ID), _get_single_cmk_arn(AWS_KMS_KEY_ID_2)] + + def setup_kms_master_key_provider(cache=True): - """Reads the test_values config file and builds the requested KMS Master Key Provider.""" + """Build an AWS KMS Master Key Provider.""" global _KMS_MKP # pylint: disable=global-statement if cache and _KMS_MKP is not None: return _KMS_MKP cmk_arn = get_cmk_arn() kms_master_key_provider = KMSMasterKeyProvider() - kms_master_key_provider.add_master_key(cmk_arn) + kms_master_key_provider.add_master_key(cmk_arn.encode("utf-8")) if cache: _KMS_MKP = kms_master_key_provider @@ -53,16 +66,42 @@ def setup_kms_master_key_provider(cache=True): def setup_kms_master_key_provider_with_botocore_session(cache=True): - """Reads the test_values config file and builds the requested KMS Master Key Provider with botocore_session.""" + """Build an AWS KMS Master Key Provider with an explicit botocore_session.""" global _KMS_MKP_BOTO # pylint: disable=global-statement if cache and _KMS_MKP_BOTO is not None: return _KMS_MKP_BOTO cmk_arn = get_cmk_arn() kms_master_key_provider = KMSMasterKeyProvider(botocore_session=botocore.session.Session()) - kms_master_key_provider.add_master_key(cmk_arn) + kms_master_key_provider.add_master_key(cmk_arn.encode("utf-8")) if cache: _KMS_MKP_BOTO = kms_master_key_provider return kms_master_key_provider + + +def build_aws_kms_keyring(generate=True, cache=True): + """Build an AWS KMS keyring.""" + global _KMS_KEYRING # pylint: disable=global-statement + if cache and _KMS_KEYRING is not None: + return _KMS_KEYRING + + cmk_arn = get_cmk_arn() + + if generate: + kwargs = dict(generator_key_id=cmk_arn) + else: + kwargs = dict(key_ids=[cmk_arn]) + + keyring = KmsKeyring(**kwargs) + + if cache: + _KMS_KEYRING = keyring + + return keyring + + +@pytest.fixture +def aws_kms_keyring(): + return build_aws_kms_keyring() diff --git a/test/integration/key_providers/__init__.py b/test/integration/key_providers/__init__.py new file mode 100644 index 000000000..d548f9b1f --- /dev/null +++ b/test/integration/key_providers/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Dummy stub to make linters work better.""" diff --git a/test/integration/key_providers/test_kms.py b/test/integration/key_providers/test_kms.py new file mode 100644 index 000000000..59c699f70 --- /dev/null +++ b/test/integration/key_providers/test_kms.py @@ -0,0 +1,31 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Integration tests for ``aws_encryption_sdk.key_provider.kms``.""" +from test.integration.integration_test_utils import setup_kms_master_key_provider_with_botocore_session + +import pytest +from botocore.exceptions import BotoCoreError + +from aws_encryption_sdk.key_providers.kms import KMSMasterKeyProvider + +pytestmark = [pytest.mark.integ] + + +def test_remove_bad_client(): + test = KMSMasterKeyProvider() + fake_region = "us-fakey-12" + test.add_regional_client(fake_region) + + with pytest.raises(BotoCoreError): + test._regional_clients[fake_region].list_keys() + + assert fake_region not in test._regional_clients + + +def test_regional_client_does_not_modify_botocore_session(caplog): + mkp = setup_kms_master_key_provider_with_botocore_session() + fake_region = "us-fakey-12" + + assert mkp.config.botocore_session.get_config_variable("region") != fake_region + mkp.add_regional_client(fake_region) + assert mkp.config.botocore_session.get_config_variable("region") != fake_region diff --git a/test/integration/keyrings/__init__.py b/test/integration/keyrings/__init__.py new file mode 100644 index 000000000..d548f9b1f --- /dev/null +++ b/test/integration/keyrings/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Dummy stub to make linters work better.""" diff --git a/test/integration/keyrings/aws_kms/__init__.py b/test/integration/keyrings/aws_kms/__init__.py new file mode 100644 index 000000000..d548f9b1f --- /dev/null +++ b/test/integration/keyrings/aws_kms/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Dummy stub to make linters work better.""" diff --git a/test/integration/keyrings/aws_kms/test_client_cache.py b/test/integration/keyrings/aws_kms/test_client_cache.py new file mode 100644 index 000000000..6ab1a05d5 --- /dev/null +++ b/test/integration/keyrings/aws_kms/test_client_cache.py @@ -0,0 +1,49 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Integration tests for ``aws_encryption_sdk.keyrings.aws_kms.client_cache``.""" +import pytest +from botocore.config import Config +from botocore.exceptions import BotoCoreError +from botocore.session import Session + +from aws_encryption_sdk.keyrings.aws_kms._client_cache import ClientCache + +pytestmark = [pytest.mark.integ] + + +def test_client_cache_removes_bad_client(): + cache = ClientCache(botocore_session=Session(), client_config=Config()) + fake_region = "us-fake-1" + + initial_client = cache.client(fake_region, "kms") + + assert fake_region in cache._cache + + with pytest.raises(BotoCoreError): + initial_client.encrypt(KeyId="foo", Plaintext=b"bar") + + assert fake_region not in cache._cache + + +def test_regional_client_does_not_modify_botocore_session(): + cache = ClientCache(botocore_session=Session(), client_config=Config()) + fake_region = "us-fake-1" + + assert cache._botocore_session.get_config_variable("region") != fake_region + cache.client(fake_region, "kms") + assert cache._botocore_session.get_config_variable("region") != fake_region + + +def test_client_cache_remove_bad_client_when_already_removed(): + cache = ClientCache(botocore_session=Session(), client_config=Config()) + fake_region = "us-fake-1" + + initial_client = cache.client(fake_region, "kms") + + assert fake_region in cache._cache + del cache._cache[fake_region] + + with pytest.raises(BotoCoreError): + initial_client.encrypt(KeyId="foo", Plaintext=b"bar") + + assert fake_region not in cache._cache diff --git a/test/integration/test_client.py b/test/integration/test_client.py new file mode 100644 index 000000000..7f5c1e983 --- /dev/null +++ b/test/integration/test_client.py @@ -0,0 +1,116 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Integration test suite for `aws_encryption_sdk`.""" +import io +import logging + +import pytest + +import aws_encryption_sdk +from aws_encryption_sdk.identifiers import USER_AGENT_SUFFIX, Algorithm + +from .integration_test_utils import build_aws_kms_keyring, get_cmk_arn, setup_kms_master_key_provider + +pytestmark = [pytest.mark.integ] + + +VALUES = { + "plaintext_128": ( + b"\xa3\xf6\xbc\x89\x95\x15(\xc8}\\\x8d=zu^{JA\xc1\xe9\xf0&m\xe6TD\x03" + b"\x165F\x85\xae\x96\xd9~ \xa6\x13\x88\xf8\xdb\xc9\x0c\xd8\xd8\xd4\xe0" + b"\x02\xe9\xdb+\xd4l\xeaq\xf6\xba.cg\xda\xe4V\xd9\x9a\x96\xe8\xf4:\xf5" + b"\xfd\xd7\xa6\xfa\xd1\x85\xa7o\xf5\x94\xbcE\x14L\xa1\x87\xd9T\xa6\x95" + b"eZVv\xfe[\xeeJ$a<9\x1f\x97\xe1\xd6\x9dQc\x8b7n\x0f\x1e\xbd\xf5\xba" + b"\x0e\xae|%\xd8L]\xa2\xa2\x08\x1f" + ), + "encryption_context": {"key_a": "value_a", "key_b": "value_b", "key_c": "value_c"}, +} + + +@pytest.mark.parametrize( + "kwargs", + ( + pytest.param(dict(key_provider=setup_kms_master_key_provider()), id="AWS KMS master key provider"), + pytest.param( + dict(key_provider=setup_kms_master_key_provider().master_key(get_cmk_arn())), id="AWS KMS master key" + ), + pytest.param(dict(keyring=build_aws_kms_keyring()), id="AWS KMS keyring"), + ), +) +def test_encrypt_verify_user_agent_in_logs(caplog, kwargs): + caplog.set_level(level=logging.DEBUG) + + aws_encryption_sdk.encrypt(source=VALUES["plaintext_128"], **kwargs) + + assert USER_AGENT_SUFFIX in caplog.text + + +@pytest.mark.parametrize("frame_size", (pytest.param(0, id="unframed"), pytest.param(1024, id="1024 byte frame"))) +@pytest.mark.parametrize("algorithm_suite", Algorithm) +@pytest.mark.parametrize( + "encrypt_key_provider_kwargs", + ( + pytest.param(dict(key_provider=setup_kms_master_key_provider()), id="encrypt with MKP"), + pytest.param(dict(keyring=build_aws_kms_keyring()), id="encrypt with keyring"), + ), +) +@pytest.mark.parametrize( + "decrypt_key_provider_kwargs", + ( + pytest.param(dict(key_provider=setup_kms_master_key_provider()), id="decrypt with MKP"), + pytest.param(dict(keyring=build_aws_kms_keyring()), id="decrypt with keyring"), + ), +) +@pytest.mark.parametrize( + "encryption_context", + ( + pytest.param({}, id="empty encryption context"), + pytest.param(VALUES["encryption_context"], id="non-empty encryption context"), + ), +) +@pytest.mark.parametrize( + "plaintext", + ( + pytest.param(VALUES["plaintext_128"], id="plaintext smaller than frame"), + pytest.param(VALUES["plaintext_128"] * 100, id="plaintext larger than frame"), + ), +) +def test_encrypt_decrypt_cycle_aws_kms( + frame_size, algorithm_suite, encrypt_key_provider_kwargs, decrypt_key_provider_kwargs, encryption_context, plaintext +): + ciphertext, _ = aws_encryption_sdk.encrypt( + source=plaintext, + encryption_context=encryption_context, + frame_length=frame_size, + algorithm=algorithm_suite, + **encrypt_key_provider_kwargs + ) + decrypted, _ = aws_encryption_sdk.decrypt(source=ciphertext, **decrypt_key_provider_kwargs) + assert decrypted == plaintext + + +@pytest.mark.parametrize( + "plaintext", + ( + pytest.param(VALUES["plaintext_128"], id="plaintext smaller than frame"), + pytest.param(VALUES["plaintext_128"] * 100, id="plaintext larger than frame"), + ), +) +def test_encrypt_decrypt_cycle_aws_kms_streaming(plaintext): + keyring = build_aws_kms_keyring() + ciphertext = b"" + with aws_encryption_sdk.stream( + source=io.BytesIO(plaintext), keyring=keyring, mode="e", encryption_context=VALUES["encryption_context"], + ) as encryptor: + for chunk in encryptor: + ciphertext += chunk + header_1 = encryptor.header + + decrypted = b"" + with aws_encryption_sdk.stream(source=io.BytesIO(ciphertext), keyring=keyring, mode="d") as decryptor: + for chunk in decryptor: + decrypted += chunk + header_2 = decryptor.header + + assert decrypted == plaintext + assert header_1.encryption_context == header_2.encryption_context diff --git a/test/integration/test_i_aws_encrytion_sdk_client.py b/test/integration/test_i_aws_encrytion_sdk_client.py deleted file mode 100644 index 26df431dc..000000000 --- a/test/integration/test_i_aws_encrytion_sdk_client.py +++ /dev/null @@ -1,452 +0,0 @@ -# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). You -# may not use this file except in compliance with the License. A copy of -# the License is located at -# -# http://aws.amazon.com/apache2.0/ -# -# or in the "license" file accompanying this file. This file is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF -# ANY KIND, either express or implied. See the License for the specific -# language governing permissions and limitations under the License. -"""Integration test suite for `aws_encryption_sdk`.""" -import io -import logging - -import pytest -from botocore.exceptions import BotoCoreError - -import aws_encryption_sdk -from aws_encryption_sdk.identifiers import USER_AGENT_SUFFIX, Algorithm -from aws_encryption_sdk.key_providers.kms import KMSMasterKey, KMSMasterKeyProvider - -from .integration_test_utils import ( - get_cmk_arn, - setup_kms_master_key_provider, - setup_kms_master_key_provider_with_botocore_session, -) - -pytestmark = [pytest.mark.integ] - - -VALUES = { - "plaintext_128": ( - b"\xa3\xf6\xbc\x89\x95\x15(\xc8}\\\x8d=zu^{JA\xc1\xe9\xf0&m\xe6TD\x03" - b"\x165F\x85\xae\x96\xd9~ \xa6\x13\x88\xf8\xdb\xc9\x0c\xd8\xd8\xd4\xe0" - b"\x02\xe9\xdb+\xd4l\xeaq\xf6\xba.cg\xda\xe4V\xd9\x9a\x96\xe8\xf4:\xf5" - b"\xfd\xd7\xa6\xfa\xd1\x85\xa7o\xf5\x94\xbcE\x14L\xa1\x87\xd9T\xa6\x95" - b"eZVv\xfe[\xeeJ$a<9\x1f\x97\xe1\xd6\x9dQc\x8b7n\x0f\x1e\xbd\xf5\xba" - b"\x0e\xae|%\xd8L]\xa2\xa2\x08\x1f" - ), - "encryption_context": {"key_a": "value_a", "key_b": "value_b", "key_c": "value_c"}, -} - - -def test_encrypt_verify_user_agent_kms_master_key_provider(caplog): - caplog.set_level(level=logging.DEBUG) - mkp = setup_kms_master_key_provider() - mk = mkp.master_key(get_cmk_arn()) - - mk.generate_data_key(algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, encryption_context={}) - - assert USER_AGENT_SUFFIX in caplog.text - - -def test_encrypt_verify_user_agent_kms_master_key(caplog): - caplog.set_level(level=logging.DEBUG) - mk = KMSMasterKey(key_id=get_cmk_arn()) - - mk.generate_data_key(algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, encryption_context={}) - - assert USER_AGENT_SUFFIX in caplog.text - - -def test_remove_bad_client(): - test = KMSMasterKeyProvider() - test.add_regional_client("us-fakey-12") - - with pytest.raises(BotoCoreError): - test._regional_clients["us-fakey-12"].list_keys() - - assert not test._regional_clients - - -def test_regional_client_does_not_modify_botocore_session(caplog): - mkp = setup_kms_master_key_provider_with_botocore_session() - fake_region = "us-fakey-12" - - assert mkp.config.botocore_session.get_config_variable("region") != fake_region - mkp.add_regional_client(fake_region) - assert mkp.config.botocore_session.get_config_variable("region") != fake_region - - -class TestKMSThickClientIntegration(object): - @pytest.fixture(autouse=True) - def apply_fixtures(self): - self.kms_master_key_provider = setup_kms_master_key_provider() - - def test_encryption_cycle_default_algorithm_framed_stream(self): - """Test that the enrypt/decrypt cycle completes successfully - for a framed message using the default algorithm. - """ - with aws_encryption_sdk.stream( - source=io.BytesIO(VALUES["plaintext_128"]), - key_provider=self.kms_master_key_provider, - mode="e", - encryption_context=VALUES["encryption_context"], - ) as encryptor: - ciphertext = encryptor.read() - header_1 = encryptor.header - with aws_encryption_sdk.stream( - source=io.BytesIO(ciphertext), key_provider=self.kms_master_key_provider, mode="d" - ) as decryptor: - plaintext = decryptor.read() - header_2 = decryptor.header - assert plaintext == VALUES["plaintext_128"] - assert header_1.encryption_context == header_2.encryption_context - - def test_encryption_cycle_default_algorithm_framed_stream_many_lines(self): - """Test that the enrypt/decrypt cycle completes successfully - for a framed message with many frames using the default algorithm. - """ - ciphertext = b"" - with aws_encryption_sdk.stream( - source=io.BytesIO(VALUES["plaintext_128"] * 10), - key_provider=self.kms_master_key_provider, - mode="e", - encryption_context=VALUES["encryption_context"], - frame_length=128, - ) as encryptor: - for chunk in encryptor: - ciphertext += chunk - header_1 = encryptor.header - plaintext = b"" - with aws_encryption_sdk.stream( - source=io.BytesIO(ciphertext), key_provider=self.kms_master_key_provider, mode="d" - ) as decryptor: - for chunk in decryptor: - plaintext += chunk - header_2 = decryptor.header - assert plaintext == VALUES["plaintext_128"] * 10 - assert header_1.encryption_context == header_2.encryption_context - - def test_encryption_cycle_default_algorithm_non_framed(self): - """Test that the enrypt/decrypt cycle completes successfully - for a non-framed message using the default algorithm. - """ - ciphertext, _ = aws_encryption_sdk.encrypt( - source=VALUES["plaintext_128"], - key_provider=self.kms_master_key_provider, - encryption_context=VALUES["encryption_context"], - frame_length=0, - ) - plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=self.kms_master_key_provider) - assert plaintext == VALUES["plaintext_128"] - - def test_encryption_cycle_default_algorithm_non_framed_no_encryption_context(self): - """Test that the enrypt/decrypt cycle completes successfully - for a non-framed message using the default algorithm. - """ - ciphertext, _ = aws_encryption_sdk.encrypt( - source=VALUES["plaintext_128"], key_provider=self.kms_master_key_provider, frame_length=0 - ) - plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=self.kms_master_key_provider) - assert plaintext == VALUES["plaintext_128"] - - def test_encryption_cycle_default_algorithm_single_frame(self): - """Test that the enrypt/decrypt cycle completes successfully - for a single frame message using the default algorithm. - """ - ciphertext, _ = aws_encryption_sdk.encrypt( - source=VALUES["plaintext_128"], - key_provider=self.kms_master_key_provider, - encryption_context=VALUES["encryption_context"], - frame_length=1024, - ) - plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=self.kms_master_key_provider) - assert plaintext == VALUES["plaintext_128"] - - def test_encryption_cycle_default_algorithm_multiple_frames(self): - """Test that the enrypt/decrypt cycle completes successfully - for a framed message with multiple frames using the - default algorithm. - """ - ciphertext, _ = aws_encryption_sdk.encrypt( - source=VALUES["plaintext_128"] * 100, - key_provider=self.kms_master_key_provider, - encryption_context=VALUES["encryption_context"], - frame_length=1024, - ) - plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=self.kms_master_key_provider) - assert plaintext == VALUES["plaintext_128"] * 100 - - def test_encryption_cycle_aes_128_gcm_iv12_tag16_single_frame(self): - """Test that the enrypt/decrypt cycle completes successfully - for a single frame message using the aes_128_gcm_iv12_tag16 - algorithm. - """ - ciphertext, _ = aws_encryption_sdk.encrypt( - source=VALUES["plaintext_128"], - key_provider=self.kms_master_key_provider, - encryption_context=VALUES["encryption_context"], - frame_length=1024, - algorithm=Algorithm.AES_128_GCM_IV12_TAG16, - ) - plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=self.kms_master_key_provider) - assert plaintext == VALUES["plaintext_128"] - - def test_encryption_cycle_aes_128_gcm_iv12_tag16_non_framed(self): - """Test that the enrypt/decrypt cycle completes successfully - for a non-framed message using the aes_128_gcm_iv12_tag16 - algorithm. - """ - ciphertext, _ = aws_encryption_sdk.encrypt( - source=VALUES["plaintext_128"], - key_provider=self.kms_master_key_provider, - encryption_context=VALUES["encryption_context"], - frame_length=0, - algorithm=Algorithm.AES_128_GCM_IV12_TAG16, - ) - plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=self.kms_master_key_provider) - assert plaintext == VALUES["plaintext_128"] - - def test_encryption_cycle_aes_192_gcm_iv12_tag16_single_frame(self): - """Test that the enrypt/decrypt cycle completes successfully - for a single frame message using the aes_192_gcm_iv12_tag16 - algorithm. - """ - ciphertext, _ = aws_encryption_sdk.encrypt( - source=VALUES["plaintext_128"], - key_provider=self.kms_master_key_provider, - encryption_context=VALUES["encryption_context"], - frame_length=1024, - algorithm=Algorithm.AES_192_GCM_IV12_TAG16, - ) - plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=self.kms_master_key_provider) - assert plaintext == VALUES["plaintext_128"] - - def test_encryption_cycle_aes_192_gcm_iv12_tag16_non_framed(self): - """Test that the enrypt/decrypt cycle completes successfully - for a non-framed message using the aes_192_gcm_iv12_tag16 - algorithm. - """ - ciphertext, _ = aws_encryption_sdk.encrypt( - source=VALUES["plaintext_128"], - key_provider=self.kms_master_key_provider, - encryption_context=VALUES["encryption_context"], - frame_length=0, - algorithm=Algorithm.AES_192_GCM_IV12_TAG16, - ) - plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=self.kms_master_key_provider) - assert plaintext == VALUES["plaintext_128"] - - def test_encryption_cycle_aes_256_gcm_iv12_tag16_single_frame(self): - """Test that the enrypt/decrypt cycle completes successfully - for a single frame message using the aes_256_gcm_iv12_tag16 - algorithm. - """ - ciphertext, _ = aws_encryption_sdk.encrypt( - source=VALUES["plaintext_128"], - key_provider=self.kms_master_key_provider, - encryption_context=VALUES["encryption_context"], - frame_length=1024, - algorithm=Algorithm.AES_256_GCM_IV12_TAG16, - ) - plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=self.kms_master_key_provider) - assert plaintext == VALUES["plaintext_128"] - - def test_encryption_cycle_aes_256_gcm_iv12_tag16_non_framed(self): - """Test that the enrypt/decrypt cycle completes successfully - for a non-framed message using the aes_256_gcm_iv12_tag16 - algorithm. - """ - ciphertext, _ = aws_encryption_sdk.encrypt( - source=VALUES["plaintext_128"], - key_provider=self.kms_master_key_provider, - encryption_context=VALUES["encryption_context"], - frame_length=0, - algorithm=Algorithm.AES_256_GCM_IV12_TAG16, - ) - plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=self.kms_master_key_provider) - assert plaintext == VALUES["plaintext_128"] - - def test_encryption_cycle_aes_128_gcm_iv12_tag16_hkdf_sha256_single_frame(self): - """Test that the enrypt/decrypt cycle completes successfully for a - single frame message using the aes_128_gcm_iv12_tag16_hkdf_sha256 - algorithm. - """ - ciphertext, _ = aws_encryption_sdk.encrypt( - source=VALUES["plaintext_128"], - key_provider=self.kms_master_key_provider, - encryption_context=VALUES["encryption_context"], - frame_length=1024, - algorithm=Algorithm.AES_128_GCM_IV12_TAG16_HKDF_SHA256, - ) - plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=self.kms_master_key_provider) - assert plaintext == VALUES["plaintext_128"] - - def test_encryption_cycle_aes_128_gcm_iv12_tag16_hkdf_sha256_non_framed(self): - """Test that the enrypt/decrypt cycle completes successfully for a - non-framed message using the aes_128_gcm_iv12_tag16_hkdf_sha256 - algorithm. - """ - ciphertext, _ = aws_encryption_sdk.encrypt( - source=VALUES["plaintext_128"], - key_provider=self.kms_master_key_provider, - encryption_context=VALUES["encryption_context"], - frame_length=0, - algorithm=Algorithm.AES_128_GCM_IV12_TAG16_HKDF_SHA256, - ) - plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=self.kms_master_key_provider) - assert plaintext == VALUES["plaintext_128"] - - def test_encryption_cycle_aes_192_gcm_iv12_tag16_hkdf_sha256_single_frame(self): - """Test that the enrypt/decrypt cycle completes successfully for a - single frame message using the aes_192_gcm_iv12_tag16_hkdf_sha256 - algorithm. - """ - ciphertext, _ = aws_encryption_sdk.encrypt( - source=VALUES["plaintext_128"], - key_provider=self.kms_master_key_provider, - encryption_context=VALUES["encryption_context"], - frame_length=1024, - algorithm=Algorithm.AES_192_GCM_IV12_TAG16_HKDF_SHA256, - ) - plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=self.kms_master_key_provider) - assert plaintext == VALUES["plaintext_128"] - - def test_encryption_cycle_aes_192_gcm_iv12_tag16_hkdf_sha256_non_framed(self): - """Test that the enrypt/decrypt cycle completes successfully for a - non-framed message using the aes_192_gcm_iv12_tag16_hkdf_sha256 - algorithm. - """ - ciphertext, _ = aws_encryption_sdk.encrypt( - source=VALUES["plaintext_128"], - key_provider=self.kms_master_key_provider, - encryption_context=VALUES["encryption_context"], - frame_length=0, - algorithm=Algorithm.AES_192_GCM_IV12_TAG16_HKDF_SHA256, - ) - plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=self.kms_master_key_provider) - assert plaintext == VALUES["plaintext_128"] - - def test_encryption_cycle_aes_256_gcm_iv12_tag16_hkdf_sha256_single_frame(self): - """Test that the enrypt/decrypt cycle completes successfully for a - single frame message using the aes_256_gcm_iv12_tag16_hkdf_sha256 - algorithm. - """ - ciphertext, _ = aws_encryption_sdk.encrypt( - source=VALUES["plaintext_128"], - key_provider=self.kms_master_key_provider, - encryption_context=VALUES["encryption_context"], - frame_length=1024, - algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA256, - ) - plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=self.kms_master_key_provider) - assert plaintext == VALUES["plaintext_128"] - - def test_encryption_cycle_aes_256_gcm_iv12_tag16_hkdf_sha256_non_framed(self): - """Test that the enrypt/decrypt cycle completes successfully for a - non-framed message using the aes_256_gcm_iv12_tag16_hkdf_sha256 - algorithm. - """ - ciphertext, _ = aws_encryption_sdk.encrypt( - source=VALUES["plaintext_128"], - key_provider=self.kms_master_key_provider, - encryption_context=VALUES["encryption_context"], - frame_length=0, - algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA256, - ) - plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=self.kms_master_key_provider) - assert plaintext == VALUES["plaintext_128"] - - def test_encryption_cycle_aes_128_gcm_iv12_tag16_hkdf_sha256_ecdsa_p256_single_frame(self): - """Test that the enrypt/decrypt cycle completes successfully for a single - frame message using the aes_128_gcm_iv12_tag16_hkdf_sha256_ecdsa_p256 - algorithm. - """ - ciphertext, _ = aws_encryption_sdk.encrypt( - source=VALUES["plaintext_128"], - key_provider=self.kms_master_key_provider, - encryption_context=VALUES["encryption_context"], - frame_length=1024, - algorithm=Algorithm.AES_128_GCM_IV12_TAG16_HKDF_SHA256_ECDSA_P256, - ) - plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=self.kms_master_key_provider) - assert plaintext == VALUES["plaintext_128"] - - def test_encryption_cycle_aes_128_gcm_iv12_tag16_hkdf_sha256_ecdsa_p256_non_framed(self): - """Test that the enrypt/decrypt cycle completes successfully for a single - block message using the aes_128_gcm_iv12_tag16_hkdf_sha256_ecdsa_p256 - algorithm. - """ - ciphertext, _ = aws_encryption_sdk.encrypt( - source=VALUES["plaintext_128"], - key_provider=self.kms_master_key_provider, - encryption_context=VALUES["encryption_context"], - frame_length=0, - algorithm=Algorithm.AES_128_GCM_IV12_TAG16_HKDF_SHA256_ECDSA_P256, - ) - plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=self.kms_master_key_provider) - assert plaintext == VALUES["plaintext_128"] - - def test_encryption_cycle_aes_192_gcm_iv12_tag16_hkdf_sha384_ecdsa_p384_single_frame(self): - """Test that the enrypt/decrypt cycle completes successfully for a single - frame message using the aes_192_gcm_iv12_tag16_hkdf_sha384_ecdsa_p384 - algorithm. - """ - ciphertext, _ = aws_encryption_sdk.encrypt( - source=VALUES["plaintext_128"], - key_provider=self.kms_master_key_provider, - encryption_context=VALUES["encryption_context"], - frame_length=1024, - algorithm=Algorithm.AES_192_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, - ) - plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=self.kms_master_key_provider) - assert plaintext == VALUES["plaintext_128"] - - def test_encryption_cycle_aes_192_gcm_iv12_tag16_hkdf_sha384_ecdsa_p384_non_framed(self): - """Test that the enrypt/decrypt cycle completes successfully for a single - block message using the aes_192_gcm_iv12_tag16_hkdf_sha384_ecdsa_p384 - algorithm. - """ - ciphertext, _ = aws_encryption_sdk.encrypt( - source=VALUES["plaintext_128"], - key_provider=self.kms_master_key_provider, - encryption_context=VALUES["encryption_context"], - frame_length=0, - algorithm=Algorithm.AES_192_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, - ) - plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=self.kms_master_key_provider) - assert plaintext == VALUES["plaintext_128"] - - def test_encryption_cycle_aes_256_gcm_iv12_tag16_hkdf_sha384_ecdsa_p384_single_frame(self): - """Test that the enrypt/decrypt cycle completes successfully for a single - frame message using the aes_256_gcm_iv12_tag16_hkdf_sha384_ecdsa_p384 - algorithm. - """ - ciphertext, _ = aws_encryption_sdk.encrypt( - source=VALUES["plaintext_128"], - key_provider=self.kms_master_key_provider, - encryption_context=VALUES["encryption_context"], - frame_length=1024, - algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, - ) - plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=self.kms_master_key_provider) - assert plaintext == VALUES["plaintext_128"] - - def test_encryption_cycle_aes_256_gcm_iv12_tag16_hkdf_sha384_ecdsa_p384_non_framed(self): - """Test that the enrypt/decrypt cycle completes successfully for a single - block message using the aes_256_gcm_iv12_tag16_hkdf_sha384_ecdsa_p384 - algorithm. - """ - ciphertext, _ = aws_encryption_sdk.encrypt( - source=VALUES["plaintext_128"], - key_provider=self.kms_master_key_provider, - encryption_context=VALUES["encryption_context"], - frame_length=0, - algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, - ) - plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=self.kms_master_key_provider) - assert plaintext == VALUES["plaintext_128"] diff --git a/test/integration/test_i_thread_safety.py b/test/integration/test_thread_safety.py similarity index 100% rename from test/integration/test_i_thread_safety.py rename to test/integration/test_thread_safety.py diff --git a/test/requirements.txt b/test/requirements.txt index 152b5dbf4..ff9311dc4 100644 --- a/test/requirements.txt +++ b/test/requirements.txt @@ -2,3 +2,4 @@ mock pytest>=3.3.1 pytest-cov pytest-mock +moto>=1.3.14 diff --git a/test/unit/__init__.py b/test/unit/__init__.py index 53a960891..ad0e71d6c 100644 --- a/test/unit/__init__.py +++ b/test/unit/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"). You # may not use this file except in compliance with the License. A copy of @@ -10,3 +10,4 @@ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. +"""Dummy stub to make linters work better.""" diff --git a/test/unit/caches/__init__.py b/test/unit/caches/__init__.py new file mode 100644 index 000000000..ad0e71d6c --- /dev/null +++ b/test/unit/caches/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Dummy stub to make linters work better.""" diff --git a/test/unit/test_caches_base.py b/test/unit/caches/test_base.py similarity index 100% rename from test/unit/test_caches_base.py rename to test/unit/caches/test_base.py diff --git a/test/unit/test_caches.py b/test/unit/caches/test_caches.py similarity index 97% rename from test/unit/test_caches.py rename to test/unit/caches/test_caches.py index 250ad6d5b..58c1b4944 100644 --- a/test/unit/test_caches.py +++ b/test/unit/caches/test_caches.py @@ -27,7 +27,7 @@ ) from aws_encryption_sdk.identifiers import Algorithm from aws_encryption_sdk.materials_managers import DecryptionMaterialsRequest, EncryptionMaterialsRequest -from aws_encryption_sdk.structures import DataKey, MasterKeyInfo +from aws_encryption_sdk.structures import EncryptedDataKey, MasterKeyInfo pytestmark = [pytest.mark.unit, pytest.mark.local] @@ -47,19 +47,17 @@ }, "encrypted_data_keys": [ { - "key": DataKey( + "key": EncryptedDataKey( key_provider=MasterKeyInfo(provider_id="this is a provider ID", key_info=b"this is some key info"), - data_key=b"super secret key!", encrypted_data_key=b"super secret key, now with encryption!", ), "hash": b"TYoFeYuxns/FBlaw4dsRDOv25OCEKuZG9iXt5iEdJ8LU7n5glgkDAVxWUEYC4JKKykJdHkaVpxcDvNqS6UswiQ==", }, { - "key": DataKey( + "key": EncryptedDataKey( key_provider=MasterKeyInfo( provider_id="another provider ID!", key_info=b"this is some different key info" ), - data_key=b"better super secret key!", encrypted_data_key=b"better super secret key, now with encryption!", ), "hash": b"wSrDlPM2ocIj9MAtD94ULSR0Qrt1muBovBDRL+DsSTNphJEM3CZ/h3OyvYL8BR2EIXx0m7GYwv8dGtyZL2D87w==", diff --git a/test/unit/test_caches_crypto_cache_entry.py b/test/unit/caches/test_crypto_cache_entry.py similarity index 100% rename from test/unit/test_caches_crypto_cache_entry.py rename to test/unit/caches/test_crypto_cache_entry.py diff --git a/test/unit/test_caches_local.py b/test/unit/caches/test_local.py similarity index 100% rename from test/unit/test_caches_local.py rename to test/unit/caches/test_local.py diff --git a/test/unit/test_caches_null.py b/test/unit/caches/test_null.py similarity index 100% rename from test/unit/test_caches_null.py rename to test/unit/caches/test_null.py diff --git a/test/unit/internal/__init__.py b/test/unit/internal/__init__.py new file mode 100644 index 000000000..ad0e71d6c --- /dev/null +++ b/test/unit/internal/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Dummy stub to make linters work better.""" diff --git a/test/unit/internal/crypto/__init__.py b/test/unit/internal/crypto/__init__.py new file mode 100644 index 000000000..ad0e71d6c --- /dev/null +++ b/test/unit/internal/crypto/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Dummy stub to make linters work better.""" diff --git a/test/unit/internal/crypto/authentication/__init__.py b/test/unit/internal/crypto/authentication/__init__.py new file mode 100644 index 000000000..ad0e71d6c --- /dev/null +++ b/test/unit/internal/crypto/authentication/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Dummy stub to make linters work better.""" diff --git a/test/unit/test_crypto_prehashing_authenticator.py b/test/unit/internal/crypto/authentication/test_prehashing_authenticator.py similarity index 100% rename from test/unit/test_crypto_prehashing_authenticator.py rename to test/unit/internal/crypto/authentication/test_prehashing_authenticator.py diff --git a/test/unit/test_crypto_authentication_signer.py b/test/unit/internal/crypto/authentication/test_signer.py similarity index 99% rename from test/unit/test_crypto_authentication_signer.py rename to test/unit/internal/crypto/authentication/test_signer.py index eae064130..0a55b2e48 100644 --- a/test/unit/test_crypto_authentication_signer.py +++ b/test/unit/internal/crypto/authentication/test_signer.py @@ -19,7 +19,7 @@ from aws_encryption_sdk.internal.crypto.authentication import Signer from aws_encryption_sdk.internal.defaults import ALGORITHM -from .test_crypto import VALUES +from ..vectors import VALUES pytestmark = [pytest.mark.unit, pytest.mark.local] diff --git a/test/unit/test_crypto_authentication_verifier.py b/test/unit/internal/crypto/authentication/test_verifier.py similarity index 99% rename from test/unit/test_crypto_authentication_verifier.py rename to test/unit/internal/crypto/authentication/test_verifier.py index a55e8f517..e25fb78f3 100644 --- a/test/unit/test_crypto_authentication_verifier.py +++ b/test/unit/internal/crypto/authentication/test_verifier.py @@ -19,7 +19,7 @@ from aws_encryption_sdk.internal.crypto.authentication import Verifier from aws_encryption_sdk.internal.defaults import ALGORITHM -from .test_crypto import VALUES +from ..vectors import VALUES pytestmark = [pytest.mark.unit, pytest.mark.local] diff --git a/test/unit/internal/crypto/encryption/__init__.py b/test/unit/internal/crypto/encryption/__init__.py new file mode 100644 index 000000000..ad0e71d6c --- /dev/null +++ b/test/unit/internal/crypto/encryption/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Dummy stub to make linters work better.""" diff --git a/test/unit/test_crypto_encryption_decryptor.py b/test/unit/internal/crypto/encryption/test_decryptor.py similarity index 100% rename from test/unit/test_crypto_encryption_decryptor.py rename to test/unit/internal/crypto/encryption/test_decryptor.py diff --git a/test/unit/test_crypto_encryption_encryptor.py b/test/unit/internal/crypto/encryption/test_encryptor.py similarity index 100% rename from test/unit/test_crypto_encryption_encryptor.py rename to test/unit/internal/crypto/encryption/test_encryptor.py diff --git a/test/unit/test_crypto_data_keys.py b/test/unit/internal/crypto/test_data_keys.py similarity index 100% rename from test/unit/test_crypto_data_keys.py rename to test/unit/internal/crypto/test_data_keys.py diff --git a/test/unit/test_crypto_elliptic_curve.py b/test/unit/internal/crypto/test_elliptic_curve.py similarity index 99% rename from test/unit/test_crypto_elliptic_curve.py rename to test/unit/internal/crypto/test_elliptic_curve.py index b030db5c2..16dcd2686 100644 --- a/test/unit/test_crypto_elliptic_curve.py +++ b/test/unit/internal/crypto/test_elliptic_curve.py @@ -17,7 +17,6 @@ from cryptography.hazmat.primitives.asymmetric import ec from cryptography.utils import InterfaceNotImplemented from mock import MagicMock, sentinel -from pytest_mock import mocker # noqa pylint: disable=unused-import import aws_encryption_sdk.internal.crypto.elliptic_curve from aws_encryption_sdk.exceptions import NotSupportedError @@ -31,7 +30,7 @@ generate_ecc_signing_key, ) -from .test_crypto import VALUES +from .vectors import VALUES pytestmark = [pytest.mark.unit, pytest.mark.local] diff --git a/test/unit/test_crypto_wrapping_keys.py b/test/unit/internal/crypto/test_wrapping_keys.py similarity index 99% rename from test/unit/test_crypto_wrapping_keys.py rename to test/unit/internal/crypto/test_wrapping_keys.py index cb7b4489a..2bdcb0983 100644 --- a/test/unit/test_crypto_wrapping_keys.py +++ b/test/unit/internal/crypto/test_wrapping_keys.py @@ -21,7 +21,7 @@ from aws_encryption_sdk.internal.crypto.wrapping_keys import WrappingKey from aws_encryption_sdk.internal.structures import EncryptedData -from .test_crypto import VALUES +from .vectors import VALUES pytestmark = [pytest.mark.unit, pytest.mark.local] diff --git a/test/unit/test_crypto.py b/test/unit/internal/crypto/vectors.py similarity index 100% rename from test/unit/test_crypto.py rename to test/unit/internal/crypto/vectors.py diff --git a/test/unit/internal/formatting/__init__.py b/test/unit/internal/formatting/__init__.py new file mode 100644 index 000000000..ad0e71d6c --- /dev/null +++ b/test/unit/internal/formatting/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Dummy stub to make linters work better.""" diff --git a/test/unit/test_deserialize.py b/test/unit/internal/formatting/test_deserialize.py similarity index 99% rename from test/unit/test_deserialize.py rename to test/unit/internal/formatting/test_deserialize.py index 8a96ea4ca..d19093320 100644 --- a/test/unit/test_deserialize.py +++ b/test/unit/internal/formatting/test_deserialize.py @@ -23,7 +23,7 @@ from aws_encryption_sdk.identifiers import AlgorithmSuite from aws_encryption_sdk.internal.structures import EncryptedData -from .test_values import VALUES +from ...vectors import VALUES pytestmark = [pytest.mark.unit, pytest.mark.local] diff --git a/test/unit/test_encryption_context.py b/test/unit/internal/formatting/test_encryption_context.py similarity index 99% rename from test/unit/test_encryption_context.py rename to test/unit/internal/formatting/test_encryption_context.py index 187365783..443df4065 100644 --- a/test/unit/test_encryption_context.py +++ b/test/unit/internal/formatting/test_encryption_context.py @@ -18,7 +18,7 @@ from aws_encryption_sdk.exceptions import SerializationError from aws_encryption_sdk.identifiers import ContentAADString -from .test_values import VALUES +from ...vectors import VALUES pytestmark = [pytest.mark.unit, pytest.mark.local] diff --git a/test/unit/test_serialize.py b/test/unit/internal/formatting/test_serialize.py similarity index 98% rename from test/unit/test_serialize.py rename to test/unit/internal/formatting/test_serialize.py index 511048d80..7a4063472 100644 --- a/test/unit/test_serialize.py +++ b/test/unit/internal/formatting/test_serialize.py @@ -21,7 +21,7 @@ from aws_encryption_sdk.internal.structures import EncryptedData from aws_encryption_sdk.structures import EncryptedDataKey, MasterKeyInfo -from .test_values import VALUES +from ...vectors import VALUES pytestmark = [pytest.mark.unit, pytest.mark.local] @@ -325,7 +325,9 @@ def test_serialize_wrapped_key_symmetric(self): ) assert test == EncryptedDataKey( key_provider=MasterKeyInfo( - provider_id=VALUES["provider_id"], key_info=VALUES["wrapped_keys"]["serialized"]["key_info"] + provider_id=self.mock_key_provider.provider_id, + key_info=VALUES["wrapped_keys"]["serialized"]["key_info"], + key_name=VALUES["wrapped_keys"]["raw"]["key_info"], ), encrypted_data_key=VALUES["wrapped_keys"]["serialized"]["key_ciphertext"], ) diff --git a/test/unit/test_defaults.py b/test/unit/internal/test_defaults.py similarity index 100% rename from test/unit/test_defaults.py rename to test/unit/internal/test_defaults.py diff --git a/test/unit/test_internal_structures.py b/test/unit/internal/test_structures.py similarity index 97% rename from test/unit/test_internal_structures.py rename to test/unit/internal/test_structures.py index d57166982..04f4e737a 100644 --- a/test/unit/test_internal_structures.py +++ b/test/unit/internal/test_structures.py @@ -21,7 +21,7 @@ MessageNoFrameBody, ) -from .unit_test_utils import all_invalid_kwargs, all_valid_kwargs +from ..unit_test_utils import all_invalid_kwargs, all_valid_kwargs pytestmark = [pytest.mark.unit, pytest.mark.local] diff --git a/test/unit/internal/utils/__init__.py b/test/unit/internal/utils/__init__.py new file mode 100644 index 000000000..ad0e71d6c --- /dev/null +++ b/test/unit/internal/utils/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Dummy stub to make linters work better.""" diff --git a/test/unit/test_util_str_ops.py b/test/unit/internal/utils/test_str_ops.py similarity index 100% rename from test/unit/test_util_str_ops.py rename to test/unit/internal/utils/test_str_ops.py diff --git a/test/unit/test_util_streams.py b/test/unit/internal/utils/test_streams.py similarity index 96% rename from test/unit/test_util_streams.py rename to test/unit/internal/utils/test_streams.py index ab7b05152..660e4623c 100644 --- a/test/unit/test_util_streams.py +++ b/test/unit/internal/utils/test_streams.py @@ -19,7 +19,7 @@ from aws_encryption_sdk.internal.str_ops import to_bytes, to_str from aws_encryption_sdk.internal.utils.streams import InsistentReaderBytesIO, ROStream, TeeStream -from .unit_test_utils import ExactlyTwoReads, NothingButRead, SometimesIncompleteReaderIO +from ...unit_test_utils import ExactlyTwoReads, NothingButRead, SometimesIncompleteReaderIO pytestmark = [pytest.mark.unit, pytest.mark.local] diff --git a/test/unit/test_utils.py b/test/unit/internal/utils/test_utils.py similarity index 98% rename from test/unit/test_utils.py rename to test/unit/internal/utils/test_utils.py index b1374a09d..c118ba375 100644 --- a/test/unit/test_utils.py +++ b/test/unit/internal/utils/test_utils.py @@ -21,10 +21,11 @@ import aws_encryption_sdk.internal.utils from aws_encryption_sdk.exceptions import InvalidDataKeyError, SerializationError, UnknownIdentityError from aws_encryption_sdk.internal.defaults import MAX_FRAME_SIZE, MESSAGE_ID_LENGTH -from aws_encryption_sdk.structures import DataKey, EncryptedDataKey, MasterKeyInfo, RawDataKey +from aws_encryption_sdk.keyrings.base import EncryptedDataKey +from aws_encryption_sdk.structures import DataKey, MasterKeyInfo, RawDataKey -from .test_values import VALUES -from .unit_test_utils import assert_prepped_stream_identity +from ...unit_test_utils import assert_prepped_stream_identity +from ...vectors import VALUES pytestmark = [pytest.mark.unit, pytest.mark.local] diff --git a/test/unit/key_providers/__init__.py b/test/unit/key_providers/__init__.py new file mode 100644 index 000000000..ad0e71d6c --- /dev/null +++ b/test/unit/key_providers/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Dummy stub to make linters work better.""" diff --git a/test/unit/key_providers/base/__init__.py b/test/unit/key_providers/base/__init__.py new file mode 100644 index 000000000..ad0e71d6c --- /dev/null +++ b/test/unit/key_providers/base/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Dummy stub to make linters work better.""" diff --git a/test/unit/test_providers_base_master_key.py b/test/unit/key_providers/base/test_base_master_key.py similarity index 99% rename from test/unit/test_providers_base_master_key.py rename to test/unit/key_providers/base/test_base_master_key.py index 26a90ced8..4ee6c2661 100644 --- a/test/unit/test_providers_base_master_key.py +++ b/test/unit/key_providers/base/test_base_master_key.py @@ -20,7 +20,7 @@ from aws_encryption_sdk.key_providers.base import MasterKey, MasterKeyConfig, MasterKeyProvider from aws_encryption_sdk.structures import MasterKeyInfo -from .test_values import VALUES +from ...vectors import VALUES pytestmark = [pytest.mark.unit, pytest.mark.local] diff --git a/test/unit/test_providers_base_master_key_config.py b/test/unit/key_providers/base/test_base_master_key_config.py similarity index 96% rename from test/unit/test_providers_base_master_key_config.py rename to test/unit/key_providers/base/test_base_master_key_config.py index 8b6c8731a..3eb0ce406 100644 --- a/test/unit/test_providers_base_master_key_config.py +++ b/test/unit/key_providers/base/test_base_master_key_config.py @@ -15,7 +15,7 @@ from aws_encryption_sdk.key_providers.base import MasterKeyConfig -from .unit_test_utils import all_invalid_kwargs, all_valid_kwargs +from ...unit_test_utils import all_invalid_kwargs, all_valid_kwargs pytestmark = [pytest.mark.unit, pytest.mark.local] diff --git a/test/unit/test_providers_base_master_key_provider.py b/test/unit/key_providers/base/test_base_master_key_provider.py similarity index 99% rename from test/unit/test_providers_base_master_key_provider.py rename to test/unit/key_providers/base/test_base_master_key_provider.py index 44385ea17..87e9a924c 100644 --- a/test/unit/test_providers_base_master_key_provider.py +++ b/test/unit/key_providers/base/test_base_master_key_provider.py @@ -23,7 +23,7 @@ ) from aws_encryption_sdk.key_providers.base import MasterKeyProvider, MasterKeyProviderConfig -from .test_values import VALUES +from ...vectors import VALUES pytestmark = [pytest.mark.unit, pytest.mark.local] @@ -60,6 +60,12 @@ def test_repr(): ) +def test_deprecated(): + + with pytest.warns(DeprecationWarning): + MockMasterKeyProvider(provider_id="ex_provider_id", mock_new_master_key="ex_new_master_key") + + class TestBaseMasterKeyProvider(object): def test_provider_id_enforcement(self): class TestProvider(MasterKeyProvider): diff --git a/test/unit/test_providers_base_master_key_provider_config.py b/test/unit/key_providers/base/test_base_master_key_provider_config.py similarity index 93% rename from test/unit/test_providers_base_master_key_provider_config.py rename to test/unit/key_providers/base/test_base_master_key_provider_config.py index 0e21ded80..9604b84a8 100644 --- a/test/unit/test_providers_base_master_key_provider_config.py +++ b/test/unit/key_providers/base/test_base_master_key_provider_config.py @@ -11,7 +11,11 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. """Unit test suite to validate aws_encryption_sdk.key_providers.base.MasterKeyProviderConfig""" +import pytest + from aws_encryption_sdk.key_providers.base import MasterKeyProviderConfig # noqa pylint: disable=unused-import +pytestmark = [pytest.mark.unit, pytest.mark.local] + # Nothing to test at this time, but import will ensure that it exists. # If this MasterKeyProviderConfig has attributes added in the future, they should be tested here. diff --git a/test/unit/key_providers/kms/__init__.py b/test/unit/key_providers/kms/__init__.py new file mode 100644 index 000000000..ad0e71d6c --- /dev/null +++ b/test/unit/key_providers/kms/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Dummy stub to make linters work better.""" diff --git a/test/unit/test_providers_kms_master_key.py b/test/unit/key_providers/kms/test_kms_master_key.py similarity index 99% rename from test/unit/test_providers_kms_master_key.py rename to test/unit/key_providers/kms/test_kms_master_key.py index c0ab9a968..862888ad8 100644 --- a/test/unit/test_providers_kms_master_key.py +++ b/test/unit/key_providers/kms/test_kms_master_key.py @@ -22,7 +22,7 @@ from aws_encryption_sdk.key_providers.kms import KMSMasterKey, KMSMasterKeyConfig from aws_encryption_sdk.structures import DataKey, EncryptedDataKey, MasterKeyInfo -from .test_values import VALUES +from ...vectors import VALUES pytestmark = [pytest.mark.unit, pytest.mark.local] diff --git a/test/unit/test_providers_kms_master_key_config.py b/test/unit/key_providers/kms/test_kms_master_key_config.py similarity index 97% rename from test/unit/test_providers_kms_master_key_config.py rename to test/unit/key_providers/kms/test_kms_master_key_config.py index 1501c951f..224e43c7c 100644 --- a/test/unit/test_providers_kms_master_key_config.py +++ b/test/unit/key_providers/kms/test_kms_master_key_config.py @@ -17,7 +17,7 @@ from aws_encryption_sdk.key_providers.base import MasterKeyConfig from aws_encryption_sdk.key_providers.kms import _PROVIDER_ID, KMSMasterKeyConfig -from .unit_test_utils import all_invalid_kwargs, all_valid_kwargs +from ...unit_test_utils import all_invalid_kwargs, all_valid_kwargs pytestmark = [pytest.mark.unit, pytest.mark.local] diff --git a/test/unit/test_providers_kms_master_key_provider.py b/test/unit/key_providers/kms/test_kms_master_key_provider.py similarity index 98% rename from test/unit/test_providers_kms_master_key_provider.py rename to test/unit/key_providers/kms/test_kms_master_key_provider.py index b99a8bb94..f8d8dc453 100644 --- a/test/unit/test_providers_kms_master_key_provider.py +++ b/test/unit/key_providers/kms/test_kms_master_key_provider.py @@ -110,9 +110,7 @@ def test_add_regional_client_new(self): test.add_regional_client("ex_region_name") self.mock_boto3_session.assert_called_with(botocore_session=ANY) self.mock_boto3_session_instance.client.assert_called_with( - "kms", - region_name="ex_region_name", - config=test._user_agent_adding_config, + "kms", region_name="ex_region_name", config=test._user_agent_adding_config ) assert test._regional_clients["ex_region_name"] is self.mock_boto3_client_instance diff --git a/test/unit/test_providers_kms_master_key_provider_config.py b/test/unit/key_providers/kms/test_kms_master_key_provider_config.py similarity index 97% rename from test/unit/test_providers_kms_master_key_provider_config.py rename to test/unit/key_providers/kms/test_kms_master_key_provider_config.py index affa74102..9b8f9fd74 100644 --- a/test/unit/test_providers_kms_master_key_provider_config.py +++ b/test/unit/key_providers/kms/test_kms_master_key_provider_config.py @@ -17,7 +17,7 @@ from aws_encryption_sdk.key_providers.base import MasterKeyProviderConfig from aws_encryption_sdk.key_providers.kms import KMSMasterKeyProviderConfig -from .unit_test_utils import all_invalid_kwargs, all_valid_kwargs +from ...unit_test_utils import all_invalid_kwargs, all_valid_kwargs pytestmark = [pytest.mark.unit, pytest.mark.local] diff --git a/test/unit/key_providers/raw/__init__.py b/test/unit/key_providers/raw/__init__.py new file mode 100644 index 000000000..ad0e71d6c --- /dev/null +++ b/test/unit/key_providers/raw/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Dummy stub to make linters work better.""" diff --git a/test/unit/test_providers_raw_master_key.py b/test/unit/key_providers/raw/test_raw_master_key.py similarity index 99% rename from test/unit/test_providers_raw_master_key.py rename to test/unit/key_providers/raw/test_raw_master_key.py index 9abcd14c6..8b9ba658d 100644 --- a/test/unit/test_providers_raw_master_key.py +++ b/test/unit/key_providers/raw/test_raw_master_key.py @@ -20,7 +20,7 @@ from aws_encryption_sdk.key_providers.raw import RawMasterKey, RawMasterKeyConfig from aws_encryption_sdk.structures import DataKey, EncryptedDataKey, MasterKeyInfo, RawDataKey -from .test_values import VALUES +from ...vectors import VALUES pytestmark = [pytest.mark.unit, pytest.mark.local] diff --git a/test/unit/test_providers_raw_master_key_config.py b/test/unit/key_providers/raw/test_raw_master_key_config.py similarity index 96% rename from test/unit/test_providers_raw_master_key_config.py rename to test/unit/key_providers/raw/test_raw_master_key_config.py index d06feae87..bbdbc5bef 100644 --- a/test/unit/test_providers_raw_master_key_config.py +++ b/test/unit/key_providers/raw/test_raw_master_key_config.py @@ -19,7 +19,7 @@ from aws_encryption_sdk.key_providers.base import MasterKeyConfig from aws_encryption_sdk.key_providers.raw import RawMasterKeyConfig -from .unit_test_utils import all_invalid_kwargs, all_valid_kwargs +from ...unit_test_utils import all_invalid_kwargs, all_valid_kwargs pytestmark = [pytest.mark.unit, pytest.mark.local] diff --git a/test/unit/test_providers_raw_master_key_provider.py b/test/unit/key_providers/raw/test_raw_master_key_provider.py similarity index 98% rename from test/unit/test_providers_raw_master_key_provider.py rename to test/unit/key_providers/raw/test_raw_master_key_provider.py index 5128b1e22..9205d3563 100644 --- a/test/unit/test_providers_raw_master_key_provider.py +++ b/test/unit/key_providers/raw/test_raw_master_key_provider.py @@ -18,7 +18,7 @@ from aws_encryption_sdk.key_providers.base import MasterKeyProvider, MasterKeyProviderConfig from aws_encryption_sdk.key_providers.raw import RawMasterKeyProvider -from .test_values import VALUES +from ...vectors import VALUES pytestmark = [pytest.mark.unit, pytest.mark.local] diff --git a/test/unit/keyrings/__init__.py b/test/unit/keyrings/__init__.py new file mode 100644 index 000000000..ad0e71d6c --- /dev/null +++ b/test/unit/keyrings/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Dummy stub to make linters work better.""" diff --git a/test/unit/keyrings/raw/__init__.py b/test/unit/keyrings/raw/__init__.py new file mode 100644 index 000000000..ad0e71d6c --- /dev/null +++ b/test/unit/keyrings/raw/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Dummy stub to make linters work better.""" diff --git a/test/unit/keyrings/raw/test_raw_aes.py b/test/unit/keyrings/raw/test_raw_aes.py new file mode 100644 index 000000000..72961c7d4 --- /dev/null +++ b/test/unit/keyrings/raw/test_raw_aes.py @@ -0,0 +1,318 @@ +# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Unit tests for Raw AES keyring.""" + +import os + +import mock +import pytest + +import aws_encryption_sdk.key_providers.raw +import aws_encryption_sdk.keyrings.raw +from aws_encryption_sdk.exceptions import EncryptKeyError +from aws_encryption_sdk.identifiers import Algorithm, KeyringTraceFlag, WrappingAlgorithm +from aws_encryption_sdk.internal.crypto.wrapping_keys import WrappingKey +from aws_encryption_sdk.keyrings.base import Keyring +from aws_encryption_sdk.keyrings.raw import GenerateKeyError, RawAESKeyring, _generate_data_key +from aws_encryption_sdk.materials_managers import EncryptionMaterials +from aws_encryption_sdk.structures import MasterKeyInfo + +from ...unit_test_utils import ( + _DATA_KEY, + _ENCRYPTED_DATA_KEY_AES, + _ENCRYPTED_DATA_KEY_NOT_IN_KEYRING, + _ENCRYPTION_CONTEXT, + _KEY_ID, + _PROVIDER_ID, + _SIGNING_KEY, + _WRAPPING_KEY, + get_decryption_materials_with_data_encryption_key, + get_decryption_materials_without_data_encryption_key, + get_encryption_materials_with_data_encryption_key, + get_encryption_materials_without_data_encryption_key, +) + +pytestmark = [pytest.mark.unit, pytest.mark.local] + + +@pytest.fixture +def raw_aes_keyring(): + return RawAESKeyring(key_namespace=_PROVIDER_ID, key_name=_KEY_ID, wrapping_key=_WRAPPING_KEY,) + + +@pytest.fixture +def patch_generate_data_key(mocker): + mocker.patch.object(aws_encryption_sdk.keyrings.raw, "_generate_data_key") + return aws_encryption_sdk.keyrings.raw._generate_data_key + + +@pytest.fixture +def patch_decrypt_on_wrapping_key(mocker): + mocker.patch.object(WrappingKey, "decrypt") + return WrappingKey.decrypt + + +@pytest.fixture +def patch_encrypt_on_wrapping_key(mocker): + mocker.patch.object(WrappingKey, "encrypt") + return WrappingKey.encrypt + + +@pytest.fixture +def patch_os_urandom(mocker): + mocker.patch.object(os, "urandom") + return os.urandom + + +def test_parent(): + assert issubclass(RawAESKeyring, Keyring) + + +def test_valid_parameters(raw_aes_keyring): + test = raw_aes_keyring + assert test.key_name == _KEY_ID + assert test.key_namespace == _PROVIDER_ID + assert test._wrapping_algorithm == WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING + assert test._wrapping_key == _WRAPPING_KEY + + +@pytest.mark.parametrize( + "key_namespace, key_name, wrapping_algorithm, wrapping_key", + ( + (_PROVIDER_ID, None, WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING, None), + (None, None, None, None), + ( + _PROVIDER_ID, + _KEY_ID, + WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING, + WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING, + ), + ( + Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA256, + Algorithm.AES_256_GCM_IV12_TAG16, + Algorithm.AES_128_GCM_IV12_TAG16, + Algorithm.AES_128_GCM_IV12_TAG16, + ), + ), +) +def test_invalid_parameters(key_namespace, key_name, wrapping_algorithm, wrapping_key): + with pytest.raises(TypeError): + RawAESKeyring( + key_namespace=key_namespace, key_name=key_name, wrapping_key=wrapping_key, + ) + + +def test_invalid_key_length(): + with pytest.raises(ValueError) as excinfo: + RawAESKeyring( + key_namespace=_PROVIDER_ID, key_name=_KEY_ID, wrapping_key=b"012345", + ) + + excinfo.match(r"Invalid wrapping key length. Must be one of \[16, 24, 32\] bytes.") + + +def test_on_encrypt_when_data_encryption_key_given(raw_aes_keyring, patch_generate_data_key): + test_raw_aes_keyring = raw_aes_keyring + + test_raw_aes_keyring.on_encrypt(encryption_materials=get_encryption_materials_with_data_encryption_key()) + # Check if keyring is generated + assert not patch_generate_data_key.called + + +def test_keyring_trace_on_encrypt_when_data_encryption_key_given(raw_aes_keyring): + test_raw_aes_keyring = raw_aes_keyring + + test = test_raw_aes_keyring.on_encrypt(encryption_materials=get_encryption_materials_with_data_encryption_key()) + + trace_entries = [entry for entry in test.keyring_trace if entry.wrapping_key == raw_aes_keyring._key_provider] + assert len(trace_entries) == 1 + + generate_traces = [entry for entry in trace_entries if entry.flags == {KeyringTraceFlag.GENERATED_DATA_KEY}] + assert len(generate_traces) == 0 + + encrypt_traces = [ + entry + for entry in trace_entries + if entry.flags == {KeyringTraceFlag.ENCRYPTED_DATA_KEY, KeyringTraceFlag.SIGNED_ENCRYPTION_CONTEXT} + ] + assert len(encrypt_traces) == 1 + + +def test_on_encrypt_when_data_encryption_key_not_given(raw_aes_keyring): + + test_raw_aes_keyring = raw_aes_keyring + + original_number_of_encrypted_data_keys = len( + get_encryption_materials_without_data_encryption_key().encrypted_data_keys + ) + + test = test_raw_aes_keyring.on_encrypt(encryption_materials=get_encryption_materials_without_data_encryption_key()) + + # Check if data key is generated + assert test.data_encryption_key is not None + + trace_entries = [entry for entry in test.keyring_trace if entry.wrapping_key == raw_aes_keyring._key_provider] + assert len(trace_entries) == 2 + + generate_traces = [entry for entry in trace_entries if entry.flags == {KeyringTraceFlag.GENERATED_DATA_KEY}] + assert len(generate_traces) == 1 + + encrypt_traces = [ + entry + for entry in trace_entries + if entry.flags == {KeyringTraceFlag.ENCRYPTED_DATA_KEY, KeyringTraceFlag.SIGNED_ENCRYPTION_CONTEXT} + ] + assert len(encrypt_traces) == 1 + + assert len(test.encrypted_data_keys) == original_number_of_encrypted_data_keys + 1 + + +def test_on_encrypt_cannot_encrypt(patch_encrypt_on_wrapping_key, raw_aes_keyring): + patch_encrypt_on_wrapping_key.side_effect = Exception("ENCRYPT FAIL") + + with pytest.raises(EncryptKeyError) as excinfo: + raw_aes_keyring.on_encrypt(get_encryption_materials_without_data_encryption_key()) + + excinfo.match("Raw AES keyring unable to encrypt data key") + + +@pytest.mark.parametrize( + "decryption_materials, edk", + ( + (get_decryption_materials_with_data_encryption_key(), [_ENCRYPTED_DATA_KEY_AES]), + (get_decryption_materials_with_data_encryption_key(), []), + ), +) +def test_on_decrypt_when_data_key_given(raw_aes_keyring, decryption_materials, edk, patch_decrypt_on_wrapping_key): + test_raw_aes_keyring = raw_aes_keyring + test_raw_aes_keyring.on_decrypt(decryption_materials=decryption_materials, encrypted_data_keys=edk) + assert not patch_decrypt_on_wrapping_key.called + + +def test_on_decrypt_keyring_trace_when_data_key_given(raw_aes_keyring): + test_raw_aes_keyring = raw_aes_keyring + test = test_raw_aes_keyring.on_decrypt( + decryption_materials=get_decryption_materials_with_data_encryption_key(), + encrypted_data_keys=[_ENCRYPTED_DATA_KEY_AES], + ) + + trace_entries = [entry for entry in test.keyring_trace if entry.wrapping_key == raw_aes_keyring._key_provider] + assert len(trace_entries) == 0 + + +@pytest.mark.parametrize( + "decryption_materials, edk", + ( + (get_decryption_materials_without_data_encryption_key(), []), + (get_encryption_materials_without_data_encryption_key(), [_ENCRYPTED_DATA_KEY_NOT_IN_KEYRING]), + ), +) +def test_on_decrypt_when_data_key_and_edk_not_provided( + raw_aes_keyring, decryption_materials, edk, patch_decrypt_on_wrapping_key +): + test_raw_aes_keyring = raw_aes_keyring + + test = test_raw_aes_keyring.on_decrypt(decryption_materials=decryption_materials, encrypted_data_keys=edk) + assert not patch_decrypt_on_wrapping_key.called + + trace_entries = [entry for entry in test.keyring_trace if entry.wrapping_key == raw_aes_keyring._key_provider] + assert len(trace_entries) == 0 + + assert test.data_encryption_key is None + + +def test_on_decrypt_when_data_key_not_provided_and_edk_provided(raw_aes_keyring, patch_decrypt_on_wrapping_key): + patch_decrypt_on_wrapping_key.return_value = _DATA_KEY + test_raw_aes_keyring = raw_aes_keyring + test_raw_aes_keyring.on_decrypt( + decryption_materials=get_decryption_materials_without_data_encryption_key(), + encrypted_data_keys=[_ENCRYPTED_DATA_KEY_AES], + ) + patch_decrypt_on_wrapping_key.assert_called_once_with( + encrypted_wrapped_data_key=mock.ANY, encryption_context=mock.ANY + ) + + +def test_on_decrypt_keyring_trace_when_data_key_not_provided_and_edk_provided(raw_aes_keyring): + test_raw_aes_keyring = raw_aes_keyring + + test = test_raw_aes_keyring.on_decrypt( + decryption_materials=get_decryption_materials_without_data_encryption_key(), + encrypted_data_keys=[_ENCRYPTED_DATA_KEY_AES], + ) + + trace_entries = [entry for entry in test.keyring_trace if entry.wrapping_key == raw_aes_keyring._key_provider] + assert len(trace_entries) == 1 + + decrypt_traces = [ + entry + for entry in trace_entries + if entry.flags == {KeyringTraceFlag.DECRYPTED_DATA_KEY, KeyringTraceFlag.VERIFIED_ENCRYPTION_CONTEXT} + ] + assert len(decrypt_traces) == 1 + + +def test_on_decrypt_continues_through_edks_on_failure(raw_aes_keyring, patch_decrypt_on_wrapping_key): + patch_decrypt_on_wrapping_key.side_effect = (Exception("DECRYPT FAIL"), _DATA_KEY) + + test = raw_aes_keyring.on_decrypt( + decryption_materials=get_decryption_materials_without_data_encryption_key(), + encrypted_data_keys=(_ENCRYPTED_DATA_KEY_AES, _ENCRYPTED_DATA_KEY_AES), + ) + + assert test.data_encryption_key is not None + assert patch_decrypt_on_wrapping_key.call_count == 2 + + +def test_generate_data_key_error_when_data_key_not_generated(patch_os_urandom): + patch_os_urandom.side_effect = NotImplementedError + with pytest.raises(GenerateKeyError) as exc_info: + _generate_data_key( + encryption_materials=get_encryption_materials_without_data_encryption_key(), + key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID), + ) + assert exc_info.match("Unable to generate data encryption key.") + + +def test_generate_data_key_error_when_data_key_exists(): + with pytest.raises(TypeError) as exc_info: + _generate_data_key( + encryption_materials=get_encryption_materials_with_data_encryption_key(), + key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID), + ) + assert exc_info.match("Data encryption key already exists.") + + +def test_generate_data_key_keyring_trace(): + encryption_materials_without_data_key = EncryptionMaterials( + algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + encryption_context=_ENCRYPTION_CONTEXT, + signing_key=_SIGNING_KEY, + ) + key_provider_info = MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID) + new_materials = _generate_data_key( + encryption_materials=encryption_materials_without_data_key, key_provider=key_provider_info, + ) + + assert new_materials is not encryption_materials_without_data_key + assert encryption_materials_without_data_key.data_encryption_key is None + assert not encryption_materials_without_data_key.keyring_trace + + assert new_materials.data_encryption_key is not None + assert new_materials.data_encryption_key.key_provider == key_provider_info + + trace_entries = [entry for entry in new_materials.keyring_trace if entry.wrapping_key == key_provider_info] + assert len(trace_entries) == 1 + + generate_traces = [entry for entry in trace_entries if entry.flags == {KeyringTraceFlag.GENERATED_DATA_KEY}] + assert len(generate_traces) == 1 diff --git a/test/unit/keyrings/raw/test_raw_rsa.py b/test/unit/keyrings/raw/test_raw_rsa.py new file mode 100644 index 000000000..5416ae24d --- /dev/null +++ b/test/unit/keyrings/raw/test_raw_rsa.py @@ -0,0 +1,321 @@ +# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Unit tests for Raw AES keyring.""" + +import pytest +from cryptography.hazmat.primitives.asymmetric import rsa + +import aws_encryption_sdk.key_providers.raw +import aws_encryption_sdk.keyrings.raw +from aws_encryption_sdk.exceptions import EncryptKeyError +from aws_encryption_sdk.identifiers import KeyringTraceFlag, WrappingAlgorithm +from aws_encryption_sdk.internal.crypto.wrapping_keys import WrappingKey +from aws_encryption_sdk.keyrings.base import Keyring +from aws_encryption_sdk.keyrings.raw import RawRSAKeyring + +from ...unit_test_utils import ( + _BACKEND, + _DATA_KEY, + _ENCRYPTED_DATA_KEY_AES, + _ENCRYPTED_DATA_KEY_RSA, + _ENCRYPTION_CONTEXT, + _KEY_ID, + _KEY_SIZE, + _PROVIDER_ID, + _PUBLIC_EXPONENT, + get_decryption_materials_with_data_encryption_key, + get_decryption_materials_without_data_encryption_key, + get_encryption_materials_with_data_encryption_key, + get_encryption_materials_without_data_encryption_key, +) +from ...vectors import VALUES + +pytestmark = [pytest.mark.unit, pytest.mark.local] + + +@pytest.fixture +def raw_rsa_keyring(): + return RawRSAKeyring.from_pem_encoding( + key_namespace=_PROVIDER_ID, + key_name=_KEY_ID, + wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, + private_encoded_key=VALUES["private_rsa_key_bytes"][1], + ) + + +def raw_rsa_private_key(): + return rsa.generate_private_key(public_exponent=_PUBLIC_EXPONENT, key_size=_KEY_SIZE, backend=_BACKEND) + + +@pytest.fixture +def patch_generate_data_key(mocker): + mocker.patch.object(aws_encryption_sdk.keyrings.raw, "_generate_data_key") + return aws_encryption_sdk.keyrings.raw._generate_data_key + + +@pytest.fixture +def patch_decrypt_on_wrapping_key(mocker): + mocker.patch.object(WrappingKey, "decrypt") + return WrappingKey.decrypt + + +@pytest.fixture +def patch_os_urandom(mocker): + mocker.patch.object(aws_encryption_sdk.key_providers.raw.os, "urandom") + return aws_encryption_sdk.key_providers.raw.os.urandom + + +def test_parent(): + assert issubclass(RawRSAKeyring, Keyring) + + +def test_valid_parameters(raw_rsa_keyring): + test = raw_rsa_keyring + assert test.key_namespace == _PROVIDER_ID + assert test.key_name == _KEY_ID + assert test._wrapping_algorithm == WrappingAlgorithm.RSA_OAEP_SHA256_MGF1 + assert isinstance(test._private_wrapping_key, rsa.RSAPrivateKey) + + +@pytest.mark.parametrize( + "key_namespace, key_name, wrapping_algorithm, private_wrapping_key, public_wrapping_key", + ( + (_PROVIDER_ID, None, WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, raw_rsa_private_key(), None), + (None, None, None, None, None), + (_PROVIDER_ID, _KEY_ID, WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, None), + (None, None, None, raw_rsa_private_key(), raw_rsa_private_key().public_key()), + (len(_PROVIDER_ID), len(_KEY_ID), _PROVIDER_ID, _PROVIDER_ID, _KEY_ID), + ), +) +def test_invalid_parameters(key_namespace, key_name, wrapping_algorithm, private_wrapping_key, public_wrapping_key): + with pytest.raises(TypeError): + RawRSAKeyring( + key_namespace=key_namespace, + key_name=key_name, + wrapping_algorithm=wrapping_algorithm, + private_wrapping_key=private_wrapping_key, + public_wrapping_key=public_wrapping_key, + ) + + +@pytest.mark.parametrize( + "wrapping_algorithm", + ( + WrappingAlgorithm.AES_128_GCM_IV12_TAG16_NO_PADDING, + WrappingAlgorithm.AES_192_GCM_IV12_TAG16_NO_PADDING, + WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING, + ), +) +def test_invalid_wrapping_algorithm_suite(wrapping_algorithm): + with pytest.raises(ValueError): + RawRSAKeyring( + key_namespace=_PROVIDER_ID, + key_name=_KEY_ID, + wrapping_algorithm=wrapping_algorithm, + private_wrapping_key=raw_rsa_private_key(), + ) + + +def test_public_and_private_key_not_provided(): + with pytest.raises(TypeError) as exc_info: + RawRSAKeyring( + key_namespace=_PROVIDER_ID, key_name=_KEY_ID, wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1 + ) + assert exc_info.match("At least one of public key or private key must be provided.") + + +def test_on_encrypt_when_data_encryption_key_given(raw_rsa_keyring, patch_generate_data_key): + test_raw_rsa_keyring = raw_rsa_keyring + + test_raw_rsa_keyring.on_encrypt(encryption_materials=get_encryption_materials_with_data_encryption_key()) + # Check if keyring is generated + assert not patch_generate_data_key.called + + +def test_on_encrypt_no_public_key(raw_rsa_keyring): + raw_rsa_keyring._public_wrapping_key = None + + with pytest.raises(EncryptKeyError) as excinfo: + raw_rsa_keyring.on_encrypt(encryption_materials=get_encryption_materials_without_data_encryption_key()) + + excinfo.match("Raw RSA keyring unable to encrypt data key: no public key available") + + +def test_on_encrypt_keyring_trace_when_data_encryption_key_given(raw_rsa_keyring): + materials = get_encryption_materials_with_data_encryption_key() + test = raw_rsa_keyring.on_encrypt(encryption_materials=materials) + assert test is not materials + + trace_entries = [entry for entry in test.keyring_trace if entry.wrapping_key == raw_rsa_keyring._key_provider] + assert len(trace_entries) == 1 + + encrypt_traces = [entry for entry in trace_entries if entry.flags == {KeyringTraceFlag.ENCRYPTED_DATA_KEY}] + assert len(encrypt_traces) == 1 + + generate_traces = [entry for entry in trace_entries if entry.flags == {KeyringTraceFlag.GENERATED_DATA_KEY}] + assert len(generate_traces) == 0 + + +def test_on_encrypt_when_data_encryption_key_not_given(raw_rsa_keyring): + test_raw_rsa_keyring = raw_rsa_keyring + + original_number_of_encrypted_data_keys = len( + get_encryption_materials_without_data_encryption_key().encrypted_data_keys + ) + + test = test_raw_rsa_keyring.on_encrypt(encryption_materials=get_encryption_materials_without_data_encryption_key()) + + trace_entries = [entry for entry in test.keyring_trace if entry.wrapping_key == raw_rsa_keyring._key_provider] + assert len(trace_entries) == 2 + + encrypt_traces = [entry for entry in trace_entries if entry.flags == {KeyringTraceFlag.ENCRYPTED_DATA_KEY}] + assert len(encrypt_traces) == 1 + + generate_traces = [entry for entry in trace_entries if entry.flags == {KeyringTraceFlag.GENERATED_DATA_KEY}] + assert len(generate_traces) == 1 + + assert test.data_encryption_key.data_key is not None + + assert len(test.encrypted_data_keys) == original_number_of_encrypted_data_keys + 1 + + +def test_on_encrypt_cannot_encrypt(raw_rsa_keyring, mocker): + encrypt_patch = mocker.patch.object(raw_rsa_keyring._public_wrapping_key, "encrypt") + encrypt_patch.side_effect = Exception("ENCRYPT FAIL") + + with pytest.raises(EncryptKeyError) as excinfo: + raw_rsa_keyring.on_encrypt(encryption_materials=get_encryption_materials_without_data_encryption_key()) + + excinfo.match("Raw RSA keyring unable to encrypt data key") + + +def test_on_decrypt_when_data_key_given(raw_rsa_keyring, patch_decrypt_on_wrapping_key): + test_raw_rsa_keyring = raw_rsa_keyring + test_raw_rsa_keyring.on_decrypt( + decryption_materials=get_decryption_materials_with_data_encryption_key(), + encrypted_data_keys=[_ENCRYPTED_DATA_KEY_RSA], + ) + assert not patch_decrypt_on_wrapping_key.called + + +def test_on_decrypt_no_private_key(raw_rsa_keyring): + raw_rsa_keyring._private_wrapping_key = None + + materials = get_decryption_materials_without_data_encryption_key() + test = raw_rsa_keyring.on_decrypt(decryption_materials=materials, encrypted_data_keys=[_ENCRYPTED_DATA_KEY_RSA],) + + assert test is materials + + +def test_on_decrypt_keyring_trace_when_data_key_given(raw_rsa_keyring): + test_raw_rsa_keyring = raw_rsa_keyring + test = test_raw_rsa_keyring.on_decrypt( + decryption_materials=get_decryption_materials_with_data_encryption_key(), + encrypted_data_keys=[_ENCRYPTED_DATA_KEY_RSA], + ) + trace_entries = [entry for entry in test.keyring_trace if entry.wrapping_key == raw_rsa_keyring._key_provider] + assert len(trace_entries) == 0 + + +def test_on_decrypt_when_data_key_and_edk_not_provided(raw_rsa_keyring, patch_decrypt_on_wrapping_key): + test_raw_rsa_keyring = raw_rsa_keyring + + test = test_raw_rsa_keyring.on_decrypt( + decryption_materials=get_decryption_materials_without_data_encryption_key(), encrypted_data_keys=[] + ) + assert not patch_decrypt_on_wrapping_key.called + + trace_entries = [entry for entry in test.keyring_trace if entry.wrapping_key == raw_rsa_keyring._key_provider] + assert len(trace_entries) == 0 + + assert test.data_encryption_key is None + + +def test_on_decrypt_when_data_key_not_provided_and_no_know_edks(raw_rsa_keyring, mocker): + patched_wrapping_key_decrypt = mocker.patch.object(raw_rsa_keyring._private_wrapping_key, "decrypt") + + test = raw_rsa_keyring.on_decrypt( + decryption_materials=get_decryption_materials_without_data_encryption_key(), + encrypted_data_keys=[_ENCRYPTED_DATA_KEY_AES], + ) + + assert not patched_wrapping_key_decrypt.called + + assert test.data_encryption_key is None + + +def test_on_decrypt_when_data_key_not_provided_and_edk_not_in_keyring(raw_rsa_keyring, patch_decrypt_on_wrapping_key): + test_raw_rsa_keyring = raw_rsa_keyring + + test = test_raw_rsa_keyring.on_decrypt( + decryption_materials=get_decryption_materials_without_data_encryption_key(), + encrypted_data_keys=[_ENCRYPTED_DATA_KEY_RSA], + ) + assert not patch_decrypt_on_wrapping_key.called + + trace_entries = [entry for entry in test.keyring_trace if entry.wrapping_key == raw_rsa_keyring._key_provider] + assert not trace_entries + + assert test.data_encryption_key is None + + +def test_on_decrypt_when_data_key_not_provided_and_edk_provided(raw_rsa_keyring, patch_decrypt_on_wrapping_key): + patch_decrypt_on_wrapping_key.return_value = _DATA_KEY + test_raw_rsa_keyring = raw_rsa_keyring + + test_raw_rsa_keyring.on_decrypt( + decryption_materials=get_decryption_materials_without_data_encryption_key(), + encrypted_data_keys=[_ENCRYPTED_DATA_KEY_RSA], + ) + assert patch_decrypt_on_wrapping_key.called_once_with( + encrypted_wrapped_data_key=_ENCRYPTED_DATA_KEY_RSA, encryption_context=_ENCRYPTION_CONTEXT + ) + + +def test_on_decrypt_keyring_trace_when_data_key_not_provided_and_edk_provided(raw_rsa_keyring): + test_raw_rsa_keyring = raw_rsa_keyring + + test = test_raw_rsa_keyring.on_decrypt( + decryption_materials=get_decryption_materials_without_data_encryption_key(), + encrypted_data_keys=test_raw_rsa_keyring.on_encrypt( + encryption_materials=get_encryption_materials_without_data_encryption_key() + ).encrypted_data_keys, + ) + + trace_entries = [entry for entry in test.keyring_trace if entry.wrapping_key == raw_rsa_keyring._key_provider] + assert len(trace_entries) == 1 + + decrypt_traces = [entry for entry in trace_entries if entry.flags == {KeyringTraceFlag.DECRYPTED_DATA_KEY}] + assert len(decrypt_traces) == 1 + + assert test.data_encryption_key is not None + + +def test_on_decrypt_continues_through_edks_on_failure(raw_rsa_keyring, mocker): + patched_wrapping_key_decrypt = mocker.patch.object(raw_rsa_keyring._private_wrapping_key, "decrypt") + patched_wrapping_key_decrypt.side_effect = (Exception("DECRYPT FAIL"), _DATA_KEY) + + test = raw_rsa_keyring.on_decrypt( + decryption_materials=get_decryption_materials_without_data_encryption_key(), + encrypted_data_keys=(_ENCRYPTED_DATA_KEY_RSA, _ENCRYPTED_DATA_KEY_RSA), + ) + + assert patched_wrapping_key_decrypt.call_count == 2 + + trace_entries = [entry for entry in test.keyring_trace if entry.wrapping_key == raw_rsa_keyring._key_provider] + assert len(trace_entries) == 1 + + decrypt_traces = [entry for entry in trace_entries if entry.flags == {KeyringTraceFlag.DECRYPTED_DATA_KEY}] + assert len(decrypt_traces) == 1 + + assert test.data_encryption_key.data_key == _DATA_KEY diff --git a/test/unit/keyrings/test_aws_kms.py b/test/unit/keyrings/test_aws_kms.py new file mode 100644 index 000000000..2adf0f656 --- /dev/null +++ b/test/unit/keyrings/test_aws_kms.py @@ -0,0 +1,198 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Unit tests for ``aws_encryption_sdk.keyrings.aws_kms``.""" +import pytest + +from aws_encryption_sdk.keyrings.aws_kms import ( + KmsKeyring, + _AwsKmsDiscoveryKeyring, + _AwsKmsSingleCmkKeyring, + _region_from_key_id, +) +from aws_encryption_sdk.keyrings.aws_kms.client_suppliers import DefaultClientSupplier +from aws_encryption_sdk.keyrings.multi import MultiKeyring + +pytestmark = [pytest.mark.unit, pytest.mark.local] + + +@pytest.mark.parametrize( + "kwargs", + ( + pytest.param(dict(client_supplier=None), id="client_supplier is invalid"), + pytest.param(dict(generator_key_id=5), id="generator_id is invalid"), + pytest.param(dict(key_ids=("foo", 5)), id="key_ids contains invalid values"), + pytest.param(dict(key_ids="some stuff"), id="key_ids is a string"), + pytest.param(dict(grant_tokens=("foo", 5)), id="grant_tokens contains invalid values"), + pytest.param(dict(grant_tokens="some stuff"), id="grant_tokens is a string"), + pytest.param(dict(generator_key_id="foo", is_discovery=True), id="generator and discovery"), + pytest.param(dict(key_ids=("foo",), is_discovery=True), id="key_ids and discovery"), + pytest.param(dict(), id="nothing"), + ), +) +def test_kms_keyring_invalid_parameters(kwargs): + with pytest.raises(TypeError): + KmsKeyring(**kwargs) + + +def test_kms_keyring_builds_correct_inner_keyring_multikeyring(): + generator_id = "foo" + child_id_1 = "bar" + child_id_2 = "baz" + grants = ("asdf", "fdsa") + supplier = DefaultClientSupplier() + + test = KmsKeyring( + generator_key_id=generator_id, key_ids=(child_id_1, child_id_2), grant_tokens=grants, client_supplier=supplier, + ) + + # We specified a generator and child IDs, so the inner keyring MUST be a multikeyring + assert isinstance(test._inner_keyring, MultiKeyring) + + # Verify that the generator is configured correctly + assert isinstance(test._inner_keyring.generator, _AwsKmsSingleCmkKeyring) + assert test._inner_keyring.generator._key_id == generator_id + assert test._inner_keyring.generator._grant_tokens == grants + assert test._inner_keyring.generator._client_supplier is supplier + + # We specified two child IDs, so there MUST be exactly two children + assert len(test._inner_keyring.children) == 2 + + # Verify that the first child is configured correctly + assert isinstance(test._inner_keyring.children[0], _AwsKmsSingleCmkKeyring) + assert test._inner_keyring.children[0]._key_id == child_id_1 + assert test._inner_keyring.children[0]._grant_tokens == grants + assert test._inner_keyring.children[0]._client_supplier is supplier + + # Verify that the second child is configured correctly + assert isinstance(test._inner_keyring.children[1], _AwsKmsSingleCmkKeyring) + assert test._inner_keyring.children[1]._key_id == child_id_2 + assert test._inner_keyring.children[1]._grant_tokens == grants + assert test._inner_keyring.children[1]._client_supplier is supplier + + +def test_kms_keyring_builds_correct_inner_keyring_multikeyring_no_generator(): + test = KmsKeyring(key_ids=("bar", "baz")) + + # We specified child IDs, so the inner keyring MUST be a multikeyring + assert isinstance(test._inner_keyring, MultiKeyring) + + # We did not specify a generator ID, so the generator MUST NOT be set + assert test._inner_keyring.generator is None + + # We specified two child IDs, so there MUST be exactly two children + assert len(test._inner_keyring.children) == 2 + + +def test_kms_keyring_builds_correct_inner_keyring_multikeyring_no_children(): + test = KmsKeyring(generator_key_id="foo") + + # We specified a generator ID, so the inner keyring MUST be a multikeyring + assert isinstance(test._inner_keyring, MultiKeyring) + + # We specified a generator ID, so the generator MUST be set + assert test._inner_keyring.generator is not None + + # We did not specify any child IDs, so the multikeyring MUST NOT contain any children + assert len(test._inner_keyring.children) == 0 + + +def test_kms_keyring_builds_correct_inner_keyring_discovery(): + grants = ("asdf", "fdas") + supplier = DefaultClientSupplier() + + test = KmsKeyring(is_discovery=True, grant_tokens=grants, client_supplier=supplier) + + # We specified neither a generator nor children, so the inner keyring MUST be a discovery keyring + assert isinstance(test._inner_keyring, _AwsKmsDiscoveryKeyring) + + # Verify that the discovery keyring is configured correctly + assert test._inner_keyring._grant_tokens == grants + assert test._inner_keyring._client_supplier is supplier + + +def test_kms_keyring_inner_keyring_on_encrypt(mocker): + mock_keyring = mocker.Mock() + + keyring = KmsKeyring(is_discovery=True) + keyring._inner_keyring = mock_keyring + + test = keyring.on_encrypt(encryption_materials=mocker.sentinel.encryption_materials) + + # on_encrypt MUST be a straight passthrough to the inner keyring + assert mock_keyring.on_encrypt.called_once_with(encryption_materials=mocker.sentinel.encryption_materials) + assert test is mock_keyring.on_encrypt.return_value + + +def test_kms_keyring_inner_keyring_on_decrypt(mocker): + mock_keyring = mocker.Mock() + + keyring = KmsKeyring(is_discovery=True) + keyring._inner_keyring = mock_keyring + + test = keyring.on_decrypt( + decryption_materials=mocker.sentinel.decryption_materials, + encrypted_data_keys=mocker.sentinel.encrypted_data_keys, + ) + + # on_decrypt MUST be a straight passthrough to the inner keyring + assert mock_keyring.on_decrypt.called_once_with( + decryption_materials=mocker.sentinel.decryption_materials, + encrypted_data_keys=mocker.sentinel.encrypted_data_keys, + ) + assert test is mock_keyring.on_decrypt.return_value + + +@pytest.mark.parametrize( + "kwargs", + ( + pytest.param(dict(key_id=None, client_supplier=DefaultClientSupplier()), id="key_id is invalid"), + pytest.param(dict(key_id="foo", client_supplier=None), id="client_supplier is invalid"), + pytest.param( + dict(key_id="foo", client_supplier=DefaultClientSupplier(), grant_tokens=("bar", 5)), + id="grant_tokens contains invalid values", + ), + pytest.param( + dict(key_id="foo", client_supplier=DefaultClientSupplier(), grant_tokens="some stuff"), + id="grant_tokens is a string", + ), + ), +) +def test_aws_kms_single_cmk_keyring_invalid_parameters(kwargs): + with pytest.raises(TypeError): + _AwsKmsSingleCmkKeyring(**kwargs) + + +@pytest.mark.parametrize( + "kwargs", + ( + pytest.param(dict(client_supplier=None), id="client_supplier is invalid"), + pytest.param( + dict(client_supplier=DefaultClientSupplier(), grant_tokens=("bar", 5)), + id="grant_tokens contains invalid values", + ), + pytest.param( + dict(client_supplier=DefaultClientSupplier(), grant_tokens="some stuff"), id="grant_tokens is a string", + ), + ), +) +def test_aws_kms_discovery_keyring_invalid_parameters(kwargs): + with pytest.raises(TypeError): + _AwsKmsDiscoveryKeyring(**kwargs) + + +@pytest.mark.parametrize( + "key_id, expected", + ( + pytest.param("foo", None, id="invalid format"), + pytest.param("alias/foo", None, id="alias name"), + pytest.param("880e7651-6f87-4c68-b84b-3220da5a7a02", None, id="key ID"), + pytest.param("arn:aws:kms:moon-base-1:111222333444:alias/foo", "moon-base-1", id="alias ARN"), + pytest.param( + "arn:aws:kms:moon-base-1:111222333444:key/880e7651-6f87-4c68-b84b-3220da5a7a02", "moon-base-1", id="CMK ARN" + ), + ), +) +def test_region_from_key_id(key_id, expected): + actual = _region_from_key_id(key_id=key_id) + + assert actual == expected diff --git a/test/unit/keyrings/test_base.py b/test/unit/keyrings/test_base.py new file mode 100644 index 000000000..08522de0a --- /dev/null +++ b/test/unit/keyrings/test_base.py @@ -0,0 +1,45 @@ +# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Unit tests for base keyring.""" + +import pytest + +from aws_encryption_sdk.identifiers import Algorithm +from aws_encryption_sdk.keyrings.base import Keyring +from aws_encryption_sdk.materials_managers import DecryptionMaterials, EncryptionMaterials + +pytestmark = [pytest.mark.unit, pytest.mark.local] + +_encryption_materials = EncryptionMaterials( + algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + encryption_context={"encryption": "context", "values": "here"}, + signing_key=b"aws-crypto-public-key", +) + +_decryption_materials = DecryptionMaterials( + algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, verification_key=b"ex_verification_key" +) + +_encrypted_data_keys = [] + + +def test_keyring_no_encrypt(): + with pytest.raises(NotImplementedError) as exc_info: + Keyring().on_encrypt(encryption_materials=_encryption_materials) + assert exc_info.match("Keyring does not implement on_encrypt function") + + +def test_keyring_no_decrypt(): + with pytest.raises(NotImplementedError) as exc_info: + Keyring().on_decrypt(decryption_materials=_decryption_materials, encrypted_data_keys=_encrypted_data_keys) + assert exc_info.match("Keyring does not implement on_decrypt function") diff --git a/test/unit/keyrings/test_multi.py b/test/unit/keyrings/test_multi.py new file mode 100644 index 000000000..97948ef63 --- /dev/null +++ b/test/unit/keyrings/test_multi.py @@ -0,0 +1,244 @@ +# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Unit tests for Multi keyring.""" + +import pytest +from mock import MagicMock +from pytest_mock import mocker # noqa pylint: disable=unused-import + +from aws_encryption_sdk.exceptions import EncryptKeyError, GenerateKeyError +from aws_encryption_sdk.identifiers import WrappingAlgorithm +from aws_encryption_sdk.internal.formatting import serialize +from aws_encryption_sdk.keyrings.base import Keyring +from aws_encryption_sdk.keyrings.multi import MultiKeyring +from aws_encryption_sdk.keyrings.raw import RawAESKeyring + +from ..unit_test_utils import ( + IdentityKeyring, + OnlyGenerateKeyring, + get_decryption_materials_with_data_key, + get_decryption_materials_without_data_key, + get_encryption_materials_with_data_key, + get_encryption_materials_with_encrypted_data_key, + get_encryption_materials_without_data_key, + get_multi_keyring_with_generator_and_children, + get_multi_keyring_with_no_children, + get_multi_keyring_with_no_generator, +) + +pytestmark = [pytest.mark.unit, pytest.mark.local] + + +_ENCRYPTION_CONTEXT = {"encryption": "context", "values": "here"} +_PROVIDER_ID = "Random Raw Keys" +_KEY_ID = b"5325b043-5843-4629-869c-64794af77ada" +_WRAPPING_KEY_AES = b"\xeby-\x80A6\x15rA8\x83#,\xe4\xab\xac`\xaf\x99Z\xc1\xce\xdb\xb6\x0f\xb7\x805\xb2\x14J3" +_SIGNING_KEY = b"aws-crypto-public-key" + + +@pytest.fixture +def identity_keyring(): + return IdentityKeyring() + + +@pytest.fixture +def keyring_which_only_generates(): + return OnlyGenerateKeyring() + + +@pytest.fixture +def mock_generator(): + mock_generator_keyring = MagicMock() + mock_generator_keyring.__class__ = RawAESKeyring + return mock_generator_keyring + + +@pytest.fixture +def mock_child_1(): + mock_child_1_keyring = MagicMock() + mock_child_1_keyring.__class__ = RawAESKeyring + return mock_child_1_keyring + + +@pytest.fixture +def mock_child_2(): + mock_child_2_keyring = MagicMock() + mock_child_2_keyring.__class__ = RawAESKeyring + return mock_child_2_keyring + + +@pytest.fixture +def mock_child_3(): + mock_child_3_keyring = MagicMock() + mock_child_3_keyring.__class__ = RawAESKeyring + mock_child_3_keyring.on_decrypt.return_value = get_decryption_materials_with_data_key() + return mock_child_3_keyring + + +@pytest.fixture +def patch_encrypt(mocker): + mocker.patch.object(serialize, "serialize_raw_master_key_prefix") + return serialize.serialize_raw_master_key_prefix + + +def test_parent(): + assert issubclass(MultiKeyring, Keyring) + + +def test_keyring_with_generator_but_no_children(): + generator_keyring = RawAESKeyring(key_namespace=_PROVIDER_ID, key_name=_KEY_ID, wrapping_key=_WRAPPING_KEY_AES,) + test_multi_keyring = MultiKeyring(generator=generator_keyring) + assert test_multi_keyring.generator is generator_keyring + assert not test_multi_keyring.children + + +def test_keyring_with_children_but_no_generator(): + children_keyring = [RawAESKeyring(key_namespace=_PROVIDER_ID, key_name=_KEY_ID, wrapping_key=_WRAPPING_KEY_AES,)] + test_multi_keyring = MultiKeyring(children=children_keyring) + assert test_multi_keyring.children is children_keyring + assert test_multi_keyring.generator is None + + +def test_keyring_with_no_generator_no_children(): + with pytest.raises(TypeError) as exc_info: + MultiKeyring() + assert exc_info.match("At least one of generator or children must be provided") + + +@pytest.mark.parametrize( + "generator, children", + ( + (WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING, None), + (WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING, get_multi_keyring_with_no_generator().children), + (None, [WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING]), + (get_multi_keyring_with_no_children().generator, [WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING]), + ), +) +def test_keyring_with_invalid_parameters(generator, children): + with pytest.raises(TypeError) as exc_info: + MultiKeyring(generator=generator, children=children) + assert exc_info.match("('children'|'generator') must be .*") + + +def test_decryption_keyring(): + test_multi_keyring = get_multi_keyring_with_generator_and_children() + assert test_multi_keyring.generator in test_multi_keyring._decryption_keyrings + for child_keyring in test_multi_keyring.children: + assert child_keyring in test_multi_keyring._decryption_keyrings + assert len(test_multi_keyring._decryption_keyrings) == len(test_multi_keyring.children) + 1 + + +def test_on_encrypt_with_no_generator_no_data_encryption_key(): + test_multi_keyring = get_multi_keyring_with_no_generator() + with pytest.raises(EncryptKeyError) as exc_info: + test_multi_keyring.on_encrypt(encryption_materials=get_encryption_materials_without_data_key()) + assert exc_info.match( + "Generator keyring not provided and encryption materials do not already contain a plaintext data key." + ) + + +def test_identity_keyring_as_generator_and_no_data_encryption_key(identity_keyring): + test_multi_keyring = MultiKeyring(generator=identity_keyring) + with pytest.raises(GenerateKeyError) as exc_info: + test_multi_keyring.on_encrypt(encryption_materials=get_encryption_materials_without_data_key()) + assert exc_info.match("Unable to generate data encryption key.") + + +def test_number_of_encrypted_data_keys_without_generator_with_children(): + test_multi_keyring = get_multi_keyring_with_no_generator() + test = test_multi_keyring.on_encrypt(encryption_materials=get_encryption_materials_with_data_key()) + assert len(test.encrypted_data_keys) == len(test_multi_keyring.children) + + +def test_number_of_encrypted_data_keys_without_children_with_generator(): + test_multi_keyring = get_multi_keyring_with_no_children() + test = test_multi_keyring.on_encrypt(encryption_materials=get_encryption_materials_with_data_key()) + assert len(test.encrypted_data_keys) == 1 + + +def test_number_of_encrypted_data_keys_with_generator_and_children(): + test_multi_keyring = get_multi_keyring_with_generator_and_children() + number_of_children = len(test_multi_keyring.children) + test = test_multi_keyring.on_encrypt(encryption_materials=get_encryption_materials_with_data_key()) + assert len(test.encrypted_data_keys) == number_of_children + 1 + + +def test_on_encrypt_when_data_encryption_key_given(mock_generator, mock_child_1, mock_child_2): + test_multi_keyring = MultiKeyring(generator=mock_generator, children=[mock_child_1, mock_child_2]) + initial_materials = get_encryption_materials_with_data_key() + new_materials = test_multi_keyring.on_encrypt(encryption_materials=initial_materials) + + assert new_materials is not initial_materials + + for keyring in test_multi_keyring._decryption_keyrings: + keyring.on_encrypt.assert_called_once() + + +def test_on_encrypt_edk_length_when_keyring_generates_but_does_not_encrypt_encryption_materials_without_data_key(): + test_multi_keyring = MultiKeyring(generator=OnlyGenerateKeyring()) + len_edk_before_encrypt = len(get_encryption_materials_without_data_key().encrypted_data_keys) + test = test_multi_keyring.on_encrypt(encryption_materials=get_encryption_materials_without_data_key()) + assert test.data_encryption_key is not None + assert len(test.encrypted_data_keys) == len_edk_before_encrypt + + +def test_on_encrypt_edk_length_when_keyring_generates_but_does_not_encrypt_encryption_materials_with_data_key(): + test_multi_keyring = MultiKeyring(generator=OnlyGenerateKeyring()) + test = test_multi_keyring.on_encrypt(encryption_materials=get_encryption_materials_with_encrypted_data_key()) + assert len(test.encrypted_data_keys) == len(get_encryption_materials_with_encrypted_data_key().encrypted_data_keys) + + +def test_on_decrypt_when_data_encryption_key_given(mock_generator, mock_child_1, mock_child_2): + test_multi_keyring = MultiKeyring(generator=mock_generator, children=[mock_child_1, mock_child_2]) + initial_materials = get_decryption_materials_with_data_key() + new_materials = test_multi_keyring.on_decrypt(decryption_materials=initial_materials, encrypted_data_keys=[]) + + assert new_materials is initial_materials + + for keyring in test_multi_keyring._decryption_keyrings: + assert not keyring.on_decrypt.called + + +def test_on_decrypt_every_keyring_called_when_data_encryption_key_not_added(mock_generator, mock_child_1, mock_child_2): + mock_generator.on_decrypt.side_effect = ( + lambda decryption_materials, encrypted_data_keys: get_decryption_materials_without_data_key() + ) + mock_child_1.on_decrypt.return_value = get_decryption_materials_without_data_key() + mock_child_2.on_decrypt.return_value = get_decryption_materials_without_data_key() + + test_multi_keyring = MultiKeyring(generator=mock_generator, children=[mock_child_1, mock_child_2]) + test_multi_keyring.on_decrypt( + decryption_materials=get_decryption_materials_without_data_key(), encrypted_data_keys=[] + ) + + for keyring in test_multi_keyring._decryption_keyrings: + assert keyring.on_decrypt.called + + +def test_no_keyring_called_after_data_encryption_key_added_when_data_encryption_key_not_given( + mock_generator, mock_child_1, mock_child_2, mock_child_3 +): + + mock_generator.on_decrypt.side_effect = ( + lambda decryption_materials, encrypted_data_keys: get_decryption_materials_without_data_key() + ) + + test_multi_keyring = MultiKeyring(generator=mock_generator, children=[mock_child_3, mock_child_1, mock_child_2]) + initial_materials = get_decryption_materials_without_data_key() + new_materials = test_multi_keyring.on_decrypt(decryption_materials=initial_materials, encrypted_data_keys=[]) + + assert new_materials is not initial_materials + assert mock_generator.on_decrypt.called + assert mock_child_3.on_decrypt.called + assert not mock_child_1.called + assert not mock_child_2.called diff --git a/test/unit/materials_managers/__init__.py b/test/unit/materials_managers/__init__.py new file mode 100644 index 000000000..ad0e71d6c --- /dev/null +++ b/test/unit/materials_managers/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Dummy stub to make linters work better.""" diff --git a/test/unit/test_material_managers_base.py b/test/unit/materials_managers/test_base.py similarity index 100% rename from test/unit/test_material_managers_base.py rename to test/unit/materials_managers/test_base.py diff --git a/test/unit/test_material_managers_caching.py b/test/unit/materials_managers/test_caching.py similarity index 92% rename from test/unit/test_material_managers_caching.py rename to test/unit/materials_managers/test_caching.py index 833d6aa53..cea3e86b6 100644 --- a/test/unit/test_material_managers_caching.py +++ b/test/unit/materials_managers/test_caching.py @@ -1,28 +1,20 @@ -# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). You -# may not use this file except in compliance with the License. A copy of -# the License is located at -# -# http://aws.amazon.com/apache2.0/ -# -# or in the "license" file accompanying this file. This file is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF -# ANY KIND, either express or implied. See the License for the specific -# language governing permissions and limitations under the License. +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 """Unit test suite for CachingCryptoMaterialsManager""" import pytest from mock import MagicMock, sentinel -from pytest_mock import mocker # noqa pylint: disable=unused-import import aws_encryption_sdk.materials_managers.caching from aws_encryption_sdk.caches.base import CryptoMaterialsCache +from aws_encryption_sdk.caches.local import LocalCryptoMaterialsCache from aws_encryption_sdk.exceptions import CacheKeyError from aws_encryption_sdk.internal.defaults import MAX_BYTES_PER_KEY, MAX_MESSAGES_PER_KEY from aws_encryption_sdk.internal.str_ops import to_bytes -from aws_encryption_sdk.key_providers.base import MasterKeyProvider from aws_encryption_sdk.materials_managers.base import CryptoMaterialsManager from aws_encryption_sdk.materials_managers.caching import CachingCryptoMaterialsManager +from aws_encryption_sdk.materials_managers.default import DefaultCryptoMaterialsManager + +from ..unit_test_utils import ephemeral_raw_aes_keyring, ephemeral_raw_aes_master_key pytestmark = [pytest.mark.unit, pytest.mark.local] @@ -54,7 +46,7 @@ def fake_encryption_request(): dict(max_messages_encrypted=None), dict(max_bytes_encrypted=None), dict(partition_name=55), - dict(master_key_provider=None, backing_materials_manager=None), + dict(master_key_provider=None, backing_materials_manager=None, keyring=None), ), ) def test_attrs_fail(invalid_kwargs): @@ -88,20 +80,26 @@ def test_custom_partition_name(patch_uuid4): assert test.partition_name == custom_partition_name -def test_mkp_to_default_cmm(mocker): - mocker.patch.object(aws_encryption_sdk.materials_managers.caching, "DefaultCryptoMaterialsManager") - mock_mkp = MagicMock(__class__=MasterKeyProvider) +def test_mkp_to_default_cmm(): + mkp = ephemeral_raw_aes_master_key() + test = CachingCryptoMaterialsManager( - cache=MagicMock(__class__=CryptoMaterialsCache), max_age=10.0, master_key_provider=mock_mkp + cache=LocalCryptoMaterialsCache(capacity=10), max_age=10.0, master_key_provider=mkp ) - aws_encryption_sdk.materials_managers.caching.DefaultCryptoMaterialsManager.assert_called_once_with( - mock_mkp - ) # noqa pylint: disable=line-too-long - assert ( - test.backing_materials_manager - is aws_encryption_sdk.materials_managers.caching.DefaultCryptoMaterialsManager.return_value - ) # noqa pylint: disable=line-too-long + assert isinstance(test.backing_materials_manager, DefaultCryptoMaterialsManager) + assert test.backing_materials_manager.master_key_provider is mkp + assert test.backing_materials_manager.keyring is None + + +def test_keyring_to_default_cmm(): + keyring = ephemeral_raw_aes_keyring() + + test = CachingCryptoMaterialsManager(cache=LocalCryptoMaterialsCache(capacity=10), max_age=10.0, keyring=keyring) + + assert isinstance(test.backing_materials_manager, DefaultCryptoMaterialsManager) + assert test.backing_materials_manager.keyring is keyring + assert test.backing_materials_manager.master_key_provider is None @pytest.mark.parametrize( diff --git a/test/unit/test_material_managers_default.py b/test/unit/materials_managers/test_default.py similarity index 59% rename from test/unit/test_material_managers_default.py rename to test/unit/materials_managers/test_default.py index 9d6bd949f..9a86e59b8 100644 --- a/test/unit/test_material_managers_default.py +++ b/test/unit/materials_managers/test_default.py @@ -1,31 +1,38 @@ -# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). You -# may not use this file except in compliance with the License. A copy of -# the License is located at -# -# http://aws.amazon.com/apache2.0/ -# -# or in the "license" file accompanying this file. This file is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF -# ANY KIND, either express or implied. See the License for the specific -# language governing permissions and limitations under the License. +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 """Test suite for aws_encryption_sdk.materials_managers.default""" import pytest from mock import MagicMock, sentinel -from pytest_mock import mocker # noqa pylint: disable=unused-import import aws_encryption_sdk.materials_managers.default -from aws_encryption_sdk.exceptions import MasterKeyProviderError, SerializationError -from aws_encryption_sdk.identifiers import Algorithm +from aws_encryption_sdk.exceptions import InvalidCryptographicMaterialsError, MasterKeyProviderError, SerializationError +from aws_encryption_sdk.identifiers import Algorithm, WrappingAlgorithm from aws_encryption_sdk.internal.defaults import ALGORITHM, ENCODED_SIGNER_KEY from aws_encryption_sdk.key_providers.base import MasterKeyProvider -from aws_encryption_sdk.materials_managers import EncryptionMaterials +from aws_encryption_sdk.materials_managers import ( + DecryptionMaterialsRequest, + EncryptionMaterials, + EncryptionMaterialsRequest, +) from aws_encryption_sdk.materials_managers.default import DefaultCryptoMaterialsManager -from aws_encryption_sdk.structures import DataKey +from aws_encryption_sdk.structures import DataKey, EncryptedDataKey, MasterKeyInfo, RawDataKey + +from ..unit_test_utils import ( + BrokenKeyring, + NoEncryptedDataKeysKeyring, + ephemeral_raw_aes_keyring, + ephemeral_raw_aes_master_key, +) pytestmark = [pytest.mark.unit, pytest.mark.local] +_DATA_KEY = DataKey( + key_provider=MasterKeyInfo(provider_id="Provider", key_info=b"Info"), + data_key=b"1234567890123456789012", + encrypted_data_key=b"asdf", +) +_ENCRYPTED_DATA_KEY = EncryptedDataKey.from_data_key(_DATA_KEY) + @pytest.fixture def patch_for_dcmm_encrypt(mocker): @@ -33,8 +40,8 @@ def patch_for_dcmm_encrypt(mocker): mock_signing_key = b"ex_signing_key" DefaultCryptoMaterialsManager._generate_signing_key_and_update_encryption_context.return_value = mock_signing_key mocker.patch.object(aws_encryption_sdk.materials_managers.default, "prepare_data_keys") - mock_data_encryption_key = MagicMock(__class__=DataKey) - mock_encrypted_data_keys = set([mock_data_encryption_key]) + mock_data_encryption_key = _DATA_KEY + mock_encrypted_data_keys = (_ENCRYPTED_DATA_KEY,) result_pair = mock_data_encryption_key, mock_encrypted_data_keys aws_encryption_sdk.materials_managers.default.prepare_data_keys.return_value = result_pair yield result_pair, mock_signing_key @@ -50,17 +57,28 @@ def patch_for_dcmm_decrypt(mocker): def build_cmm(): mock_mkp = MagicMock(__class__=MasterKeyProvider) - mock_mkp.decrypt_data_key_from_list.return_value = MagicMock(__class__=DataKey) + mock_mkp.decrypt_data_key_from_list.return_value = _DATA_KEY mock_mkp.master_keys_for_encryption.return_value = ( sentinel.primary_mk, - set([sentinel.primary_mk, sentinel.mk_a, sentinel.mk_b]), + {sentinel.primary_mk, sentinel.mk_a, sentinel.mk_b}, ) return DefaultCryptoMaterialsManager(master_key_provider=mock_mkp) -def test_attributes_fail(): +@pytest.mark.parametrize( + "kwargs", + ( + pytest.param(dict(), id="no parameters"), + pytest.param(dict(master_key_provider=None, keyring=None), id="explicit None for both"), + pytest.param( + dict(master_key_provider=ephemeral_raw_aes_master_key(), keyring=ephemeral_raw_aes_keyring()), + id="both provided", + ), + ), +) +def test_attributes_fail(kwargs): with pytest.raises(TypeError): - DefaultCryptoMaterialsManager(master_key_provider=None) + DefaultCryptoMaterialsManager(**kwargs) def test_attributes_default(): @@ -127,8 +145,8 @@ def test_get_encryption_materials(patch_for_dcmm_encrypt): ) assert isinstance(test, EncryptionMaterials) assert test.algorithm is cmm.algorithm - assert test.data_encryption_key is patch_for_dcmm_encrypt[0][0] - assert test.encrypted_data_keys is patch_for_dcmm_encrypt[0][1] + assert test.data_encryption_key == RawDataKey.from_data_key(patch_for_dcmm_encrypt[0][0]) + assert test.encrypted_data_keys == patch_for_dcmm_encrypt[0][1] assert test.encryption_context == encryption_context assert test.signing_key == patch_for_dcmm_encrypt[1] @@ -158,7 +176,7 @@ def test_get_encryption_materials_primary_mk_not_in_mks(patch_for_dcmm_encrypt): cmm = build_cmm() cmm.master_key_provider.master_keys_for_encryption.return_value = ( sentinel.primary_mk, - set([sentinel.mk_a, sentinel.mk_b]), + {sentinel.mk_a, sentinel.mk_b}, ) with pytest.raises(MasterKeyProviderError) as excinfo: @@ -232,5 +250,94 @@ def test_decrypt_materials(mocker, patch_for_dcmm_decrypt): cmm._load_verification_key_from_encryption_context.assert_called_once_with( algorithm=mock_request.algorithm, encryption_context=mock_request.encryption_context ) - assert test.data_key is cmm.master_key_provider.decrypt_data_key_from_list.return_value + assert test.data_key == RawDataKey.from_data_key(cmm.master_key_provider.decrypt_data_key_from_list.return_value) assert test.verification_key == patch_for_dcmm_decrypt + + +@pytest.mark.parametrize("algorithm_suite", Algorithm) +def test_encrypt_with_keyring_materials_incomplete(algorithm_suite): + raw_aes256_keyring = ephemeral_raw_aes_keyring(WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING) + + encrypt_cmm = DefaultCryptoMaterialsManager(keyring=NoEncryptedDataKeysKeyring(inner_keyring=raw_aes256_keyring)) + + encryption_materials_request = EncryptionMaterialsRequest( + encryption_context={}, frame_length=1024, algorithm=algorithm_suite + ) + + with pytest.raises(InvalidCryptographicMaterialsError) as excinfo: + encrypt_cmm.get_encryption_materials(encryption_materials_request) + + excinfo.match("Encryption materials are incomplete!") + + +def _broken_materials_scenarios(): + yield pytest.param(dict(break_algorithm=True), id="broken algorithm") + yield pytest.param(dict(break_encryption_context=True), id="broken encryption context") + yield pytest.param(dict(break_signing=True), id="broken signing/verification key") + + +@pytest.mark.parametrize("algorithm_suite", Algorithm) +@pytest.mark.parametrize("kwargs", _broken_materials_scenarios()) +def test_encrypt_with_keyring_materials_do_not_match_request(kwargs, algorithm_suite): + raw_aes256_keyring = ephemeral_raw_aes_keyring(WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING) + + encrypt_cmm = DefaultCryptoMaterialsManager(keyring=BrokenKeyring(inner_keyring=raw_aes256_keyring, **kwargs)) + + encryption_materials_request = EncryptionMaterialsRequest( + encryption_context={}, frame_length=1024, algorithm=algorithm_suite + ) + + with pytest.raises(InvalidCryptographicMaterialsError) as excinfo: + encrypt_cmm.get_encryption_materials(encryption_materials_request) + + excinfo.match("Encryption materials do not match request!") + + +@pytest.mark.parametrize("algorithm_suite", Algorithm) +def test_decrypt_with_keyring_materials_incomplete(algorithm_suite): + raw_aes256_keyring = ephemeral_raw_aes_keyring(WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING) + raw_aes128_keyring = ephemeral_raw_aes_keyring(WrappingAlgorithm.AES_128_GCM_IV12_TAG16_NO_PADDING) + + encrypt_cmm = DefaultCryptoMaterialsManager(keyring=raw_aes256_keyring) + decrypt_cmm = DefaultCryptoMaterialsManager(keyring=raw_aes128_keyring) + + encryption_materials_request = EncryptionMaterialsRequest( + encryption_context={}, frame_length=1024, algorithm=algorithm_suite + ) + encryption_materials = encrypt_cmm.get_encryption_materials(encryption_materials_request) + + decryption_materials_request = DecryptionMaterialsRequest( + algorithm=encryption_materials.algorithm, + encrypted_data_keys=encryption_materials.encrypted_data_keys, + encryption_context=encryption_materials.encryption_context, + ) + + with pytest.raises(InvalidCryptographicMaterialsError) as excinfo: + decrypt_cmm.decrypt_materials(decryption_materials_request) + + excinfo.match("Decryption materials are incomplete!") + + +@pytest.mark.parametrize("algorithm_suite", Algorithm) +@pytest.mark.parametrize("kwargs", _broken_materials_scenarios()) +def test_decrypt_with_keyring_materials_do_not_match_request(kwargs, algorithm_suite): + raw_aes256_keyring = ephemeral_raw_aes_keyring(WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING) + + encrypt_cmm = DefaultCryptoMaterialsManager(keyring=raw_aes256_keyring) + decrypt_cmm = DefaultCryptoMaterialsManager(keyring=BrokenKeyring(inner_keyring=raw_aes256_keyring, **kwargs)) + + encryption_materials_request = EncryptionMaterialsRequest( + encryption_context={}, frame_length=1024, algorithm=algorithm_suite + ) + encryption_materials = encrypt_cmm.get_encryption_materials(encryption_materials_request) + + decryption_materials_request = DecryptionMaterialsRequest( + algorithm=encryption_materials.algorithm, + encrypted_data_keys=encryption_materials.encrypted_data_keys, + encryption_context=encryption_materials.encryption_context, + ) + + with pytest.raises(InvalidCryptographicMaterialsError) as excinfo: + decrypt_cmm.decrypt_materials(decryption_materials_request) + + excinfo.match("Decryption materials do not match request!") diff --git a/test/unit/materials_managers/test_material_managers.py b/test/unit/materials_managers/test_material_managers.py new file mode 100644 index 000000000..62314298e --- /dev/null +++ b/test/unit/materials_managers/test_material_managers.py @@ -0,0 +1,484 @@ +# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Test suite for aws_encryption_sdk.materials_managers""" + +import pytest +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.asymmetric import ec +from mock import MagicMock + +from aws_encryption_sdk.exceptions import InvalidDataKeyError, InvalidKeyringTraceError, SignatureKeyError +from aws_encryption_sdk.identifiers import AlgorithmSuite, KeyringTraceFlag +from aws_encryption_sdk.internal.crypto.authentication import Signer, Verifier +from aws_encryption_sdk.internal.defaults import ALGORITHM +from aws_encryption_sdk.internal.utils.streams import ROStream +from aws_encryption_sdk.materials_managers import ( + CryptographicMaterials, + DecryptionMaterials, + DecryptionMaterialsRequest, + EncryptionMaterials, + EncryptionMaterialsRequest, + _data_key_to_raw_data_key, +) +from aws_encryption_sdk.structures import DataKey, EncryptedDataKey, KeyringTrace, MasterKeyInfo, RawDataKey + +pytestmark = [pytest.mark.unit, pytest.mark.local] + +_DATA_KEY = DataKey( + key_provider=MasterKeyInfo(provider_id="Provider", key_info=b"Info"), + data_key=b"1234567890123456789012", + encrypted_data_key=b"asdf", +) +_RAW_DATA_KEY = RawDataKey.from_data_key(_DATA_KEY) +_ENCRYPTED_DATA_KEY = EncryptedDataKey.from_data_key(_DATA_KEY) +_SIGNATURE_PRIVATE_KEY = ec.generate_private_key(ALGORITHM.signing_algorithm_info(), default_backend()) +_SIGNING_KEY = Signer(algorithm=ALGORITHM, key=_SIGNATURE_PRIVATE_KEY) +_VERIFICATION_KEY = Verifier(algorithm=ALGORITHM, key=_SIGNATURE_PRIVATE_KEY.public_key()) + +_VALID_KWARGS = { + "CryptographicMaterials": dict( + algorithm=ALGORITHM, + encryption_context={"additional": "data"}, + data_encryption_key=_DATA_KEY, + keyring_trace=[ + KeyringTrace( + wrapping_key=MasterKeyInfo(provider_id="Provider", key_info=b"Info"), + flags={KeyringTraceFlag.GENERATED_DATA_KEY}, + ) + ], + ), + "EncryptionMaterialsRequest": dict( + encryption_context={}, + plaintext_rostream=MagicMock(__class__=ROStream), + frame_length=5, + algorithm=ALGORITHM, + plaintext_length=5, + ), + "EncryptionMaterials": dict( + algorithm=ALGORITHM, + data_encryption_key=_DATA_KEY, + encrypted_data_keys=[], + encryption_context={}, + signing_key=_SIGNING_KEY.key_bytes(), + ), + "DecryptionMaterialsRequest": dict(algorithm=ALGORITHM, encrypted_data_keys=[], encryption_context={}), + "DecryptionMaterials": dict( + data_key=_DATA_KEY, verification_key=_VERIFICATION_KEY.key_bytes(), algorithm=ALGORITHM, encryption_context={} + ), +} +_REMOVE = object() + + +def _copy_and_update_kwargs(class_name, mod_kwargs): + kwargs = _VALID_KWARGS[class_name].copy() + kwargs.update(mod_kwargs) + purge_keys = [key for key, val in kwargs.items() if val is _REMOVE] + for key in purge_keys: + del kwargs[key] + return kwargs + + +@pytest.mark.parametrize( + "attr_class, invalid_kwargs", + ( + (CryptographicMaterials, dict(algorithm=1234)), + (CryptographicMaterials, dict(encryption_context=1234)), + (CryptographicMaterials, dict(data_encryption_key=1234)), + (CryptographicMaterials, dict(encrypted_data_keys=1234)), + (CryptographicMaterials, dict(keyring_trace=1234)), + (EncryptionMaterialsRequest, dict(encryption_context=None)), + (EncryptionMaterialsRequest, dict(frame_length="not an int")), + (EncryptionMaterialsRequest, dict(algorithm="not an Algorithm or None")), + (EncryptionMaterialsRequest, dict(plaintext_length="not an int or None")), + (EncryptionMaterials, dict(algorithm=None)), + (EncryptionMaterials, dict(encryption_context=None)), + (EncryptionMaterials, dict(signing_key=u"not bytes or None")), + (DecryptionMaterialsRequest, dict(algorithm=None)), + (DecryptionMaterialsRequest, dict(encrypted_data_keys=None)), + (DecryptionMaterialsRequest, dict(encryption_context=None)), + (DecryptionMaterials, dict(verification_key=5555)), + (DecryptionMaterials, dict(data_key=_DATA_KEY, data_encryption_key=_DATA_KEY)), + ), +) +def test_attributes_fails(attr_class, invalid_kwargs): + kwargs = _copy_and_update_kwargs(attr_class.__name__, invalid_kwargs) + with pytest.raises(TypeError): + attr_class(**kwargs) + + +@pytest.mark.parametrize( + "attr_class, kwargs_modification", + ( + (CryptographicMaterials, {}), + (EncryptionMaterials, {}), + (EncryptionMaterials, dict(data_encryption_key=_REMOVE, encrypted_data_keys=[])), + (EncryptionMaterials, dict(data_encryption_key=_REMOVE, encrypted_data_keys=_REMOVE)), + (DecryptionMaterials, {}), + (DecryptionMaterials, dict(data_key=_REMOVE, data_encryption_key=_REMOVE)), + (DecryptionMaterials, dict(data_key=_REMOVE, data_encryption_key=_RAW_DATA_KEY)), + (DecryptionMaterials, dict(data_key=_RAW_DATA_KEY, data_encryption_key=_REMOVE)), + ), +) +def test_attributes_good(attr_class, kwargs_modification): + kwargs = _copy_and_update_kwargs(attr_class.__name__, kwargs_modification) + attr_class(**kwargs) + + +def test_encryption_materials_request_attributes_defaults(): + test = EncryptionMaterialsRequest(encryption_context={}, frame_length=5) + assert test.plaintext_rostream is None + assert test.algorithm is None + assert test.plaintext_length is None + + +def test_encryption_materials_defaults(): + test = EncryptionMaterials( + algorithm=ALGORITHM, data_encryption_key=_DATA_KEY, encrypted_data_keys=[], encryption_context={} + ) + assert test.signing_key is None + + +def test_decryption_materials_defaults(): + test = DecryptionMaterials(data_key=_DATA_KEY) + assert test.verification_key is None + assert test.algorithm is None + assert test.encryption_context is None + + +def test_decryption_materials_legacy_data_key_get(): + test = DecryptionMaterials(data_encryption_key=_DATA_KEY) + + assert test.data_encryption_key == _RAW_DATA_KEY + assert test.data_key == _RAW_DATA_KEY + + +@pytest.mark.parametrize( + "data_key, expected", ((_DATA_KEY, _RAW_DATA_KEY), (_RAW_DATA_KEY, _RAW_DATA_KEY), (None, None)) +) +def test_data_key_to_raw_data_key_success(data_key, expected): + test = _data_key_to_raw_data_key(data_key=data_key) + + assert test == expected + + +def test_data_key_to_raw_data_key_fail(): + with pytest.raises(TypeError) as excinfo: + _data_key_to_raw_data_key(data_key="not a data key") + + excinfo.match("data_key must be type DataKey not str") + + +def _cryptographic_materials_attributes(): + for material in (CryptographicMaterials, EncryptionMaterials, DecryptionMaterials): + for attribute in ( + "algorithm", + "encryption_context", + "data_encryption_key", + "_keyring_trace", + "keyring_trace", + "_initialized", + ): + yield material, attribute + + for attribute in ("_encrypted_data_keys", "encrypted_data_keys", "signing_key"): + yield EncryptionMaterials, attribute + + for attribute in ("data_key", "verification_key"): + yield DecryptionMaterials, attribute + + +@pytest.mark.parametrize("material_class, attribute_name", _cryptographic_materials_attributes()) +def test_cryptographic_materials_cannot_change_attribute(material_class, attribute_name): + test = material_class(algorithm=ALGORITHM, encryption_context={}) + + with pytest.raises(AttributeError) as excinfo: + setattr(test, attribute_name, 42) + + excinfo.match("can't set attribute") + + +@pytest.mark.parametrize("material_class", (CryptographicMaterials, EncryptionMaterials, DecryptionMaterials)) +def test_immutable_keyring_trace(material_class): + materials = material_class(**_VALID_KWARGS[material_class.__name__]) + + with pytest.raises(AttributeError): + materials.keyring_trace.append(42) + + +@pytest.mark.parametrize("material_class", (CryptographicMaterials, EncryptionMaterials, DecryptionMaterials)) +def test_empty_keyring_trace(material_class): + materials = material_class(**_copy_and_update_kwargs(material_class.__name__, dict(keyring_trace=_REMOVE))) + + trace = materials.keyring_trace + + assert isinstance(trace, tuple) + assert not trace + + +def test_immutable_encrypted_data_keys(): + materials = EncryptionMaterials(**_VALID_KWARGS["EncryptionMaterials"]) + + with pytest.raises(AttributeError): + materials.encrypted_data_keys.append(42) + + +def test_empty_encrypted_data_keys(): + materials = EncryptionMaterials(**_copy_and_update_kwargs("EncryptionMaterials", dict(encrypted_data_keys=_REMOVE))) + + edks = materials.encrypted_data_keys + + assert isinstance(edks, tuple) + assert not edks + + +@pytest.mark.parametrize( + "material_class, flag", + ( + (EncryptionMaterials, KeyringTraceFlag.GENERATED_DATA_KEY), + (DecryptionMaterials, KeyringTraceFlag.DECRYPTED_DATA_KEY), + ), +) +def test_with_data_encryption_key_success(material_class, flag): + kwargs = _copy_and_update_kwargs( + material_class.__name__, dict(data_encryption_key=_REMOVE, data_key=_REMOVE, encrypted_data_keys=_REMOVE) + ) + materials = material_class(**kwargs) + + new_materials = materials.with_data_encryption_key( + data_encryption_key=RawDataKey( + key_provider=MasterKeyInfo(provider_id="a", key_info=b"b"), data_key=b"1" * ALGORITHM.kdf_input_len + ), + keyring_trace=KeyringTrace(wrapping_key=MasterKeyInfo(provider_id="a", key_info=b"b"), flags={flag}), + ) + assert new_materials is not materials + + +def _add_data_encryption_key_test_cases(): + for material_class, required_flags in ( + (EncryptionMaterials, KeyringTraceFlag.GENERATED_DATA_KEY), + (DecryptionMaterials, KeyringTraceFlag.DECRYPTED_DATA_KEY), + ): + yield ( + material_class, + dict(data_encryption_key=_RAW_DATA_KEY, data_key=_REMOVE, encrypted_data_keys=_REMOVE), + _RAW_DATA_KEY, + KeyringTrace(wrapping_key=_RAW_DATA_KEY.key_provider, flags={required_flags}), + AttributeError, + "Data encryption key is already set.", + ) + yield ( + material_class, + dict(data_encryption_key=_REMOVE, data_key=_REMOVE, encrypted_data_keys=_REMOVE), + _RAW_DATA_KEY, + KeyringTrace(wrapping_key=_RAW_DATA_KEY.key_provider, flags=set()), + InvalidKeyringTraceError, + "Keyring flags do not match action.", + ) + yield ( + material_class, + dict(data_encryption_key=_REMOVE, data_key=_REMOVE, encrypted_data_keys=_REMOVE), + RawDataKey(key_provider=MasterKeyInfo(provider_id="a", key_info=b"b"), data_key=b"asdf"), + KeyringTrace(wrapping_key=MasterKeyInfo(provider_id="c", key_info=b"d"), flags={required_flags}), + InvalidKeyringTraceError, + "Keyring trace does not match data key provider.", + ) + yield ( + material_class, + dict(data_encryption_key=_REMOVE, data_key=_REMOVE, encrypted_data_keys=_REMOVE), + RawDataKey(key_provider=_RAW_DATA_KEY.key_provider, data_key=b"1234"), + KeyringTrace(wrapping_key=_RAW_DATA_KEY.key_provider, flags={required_flags}), + InvalidDataKeyError, + r"Invalid data key length *", + ) + yield ( + DecryptionMaterials, + dict(data_encryption_key=_REMOVE, data_key=_REMOVE, encrypted_data_keys=_REMOVE, algorithm=_REMOVE), + RawDataKey(key_provider=_RAW_DATA_KEY.key_provider, data_key=b"1234"), + KeyringTrace(wrapping_key=_RAW_DATA_KEY.key_provider, flags={required_flags}), + AttributeError, + "Algorithm is not set", + ) + + +@pytest.mark.parametrize( + "material_class, mod_kwargs, data_encryption_key, keyring_trace, exception_type, exception_message", + _add_data_encryption_key_test_cases(), +) +def test_with_data_encryption_key_fail( + material_class, mod_kwargs, data_encryption_key, keyring_trace, exception_type, exception_message +): + kwargs = _copy_and_update_kwargs(material_class.__name__, mod_kwargs) + materials = material_class(**kwargs) + + with pytest.raises(exception_type) as excinfo: + materials.with_data_encryption_key(data_encryption_key=data_encryption_key, keyring_trace=keyring_trace) + + excinfo.match(exception_message) + + +def test_with_encrypted_data_key_success(): + kwargs = _copy_and_update_kwargs("EncryptionMaterials", {}) + materials = EncryptionMaterials(**kwargs) + + new_materials = materials.with_encrypted_data_key( + _ENCRYPTED_DATA_KEY, + keyring_trace=KeyringTrace( + wrapping_key=_ENCRYPTED_DATA_KEY.key_provider, flags={KeyringTraceFlag.ENCRYPTED_DATA_KEY} + ), + ) + assert new_materials is not materials + + +@pytest.mark.parametrize( + "mod_kwargs, encrypted_data_key, keyring_trace, exception_type, exception_message", + ( + ( + {}, + _ENCRYPTED_DATA_KEY, + KeyringTrace(wrapping_key=_ENCRYPTED_DATA_KEY.key_provider, flags=set()), + InvalidKeyringTraceError, + "Keyring flags do not match action.", + ), + ( + {}, + EncryptedDataKey(key_provider=MasterKeyInfo(provider_id="a", key_info=b"b"), encrypted_data_key=b"asdf"), + KeyringTrace( + wrapping_key=MasterKeyInfo(provider_id="not a match", key_info=b"really not a match"), + flags={KeyringTraceFlag.ENCRYPTED_DATA_KEY}, + ), + InvalidKeyringTraceError, + "Keyring trace does not match data key encryptor.", + ), + ( + dict(data_encryption_key=_REMOVE, encrypted_data_keys=_REMOVE), + _ENCRYPTED_DATA_KEY, + KeyringTrace(wrapping_key=_ENCRYPTED_DATA_KEY.key_provider, flags={KeyringTraceFlag.ENCRYPTED_DATA_KEY}), + AttributeError, + "Data encryption key is not set.", + ), + ), +) +def test_with_encrypted_data_key_fail(mod_kwargs, encrypted_data_key, keyring_trace, exception_type, exception_message): + kwargs = _copy_and_update_kwargs("EncryptionMaterials", mod_kwargs) + materials = EncryptionMaterials(**kwargs) + + with pytest.raises(exception_type) as excinfo: + materials.with_encrypted_data_key(encrypted_data_key=encrypted_data_key, keyring_trace=keyring_trace) + + excinfo.match(exception_message) + + +def test_with_signing_key_success(): + kwargs = _copy_and_update_kwargs("EncryptionMaterials", dict(signing_key=_REMOVE)) + materials = EncryptionMaterials(**kwargs) + + new_materials = materials.with_signing_key(signing_key=_SIGNING_KEY.key_bytes()) + assert new_materials is not materials + + +@pytest.mark.parametrize( + "mod_kwargs, signing_key, exception_type, exception_message", + ( + ({}, b"", AttributeError, "Signing key is already set."), + ( + dict(signing_key=_REMOVE, algorithm=AlgorithmSuite.AES_256_GCM_IV12_TAG16), + b"", + SignatureKeyError, + "Algorithm suite does not support signing keys.", + ), + ), +) +def test_with_signing_key_fail(mod_kwargs, signing_key, exception_type, exception_message): + kwargs = _copy_and_update_kwargs("EncryptionMaterials", mod_kwargs) + materials = EncryptionMaterials(**kwargs) + + with pytest.raises(exception_type) as excinfo: + materials.with_signing_key(signing_key=signing_key) + + excinfo.match(exception_message) + + +def test_with_verification_key_success(): + kwargs = _copy_and_update_kwargs("DecryptionMaterials", dict(verification_key=_REMOVE)) + materials = DecryptionMaterials(**kwargs) + + new_materials = materials.with_verification_key(verification_key=_VERIFICATION_KEY.key_bytes()) + assert new_materials is not materials + + +@pytest.mark.parametrize( + "mod_kwargs, verification_key, exception_type, exception_message", + ( + ({}, b"", AttributeError, "Verification key is already set."), + ( + dict(verification_key=_REMOVE, algorithm=AlgorithmSuite.AES_256_GCM_IV12_TAG16), + b"", + SignatureKeyError, + "Algorithm suite does not support signing keys.", + ), + ), +) +def test_with_verification_key_fail(mod_kwargs, verification_key, exception_type, exception_message): + kwargs = _copy_and_update_kwargs("DecryptionMaterials", mod_kwargs) + materials = DecryptionMaterials(**kwargs) + + with pytest.raises(exception_type) as excinfo: + materials.with_verification_key(verification_key=verification_key) + + excinfo.match(exception_message) + + +def test_decryption_materials_is_complete(): + materials = DecryptionMaterials(**_copy_and_update_kwargs("DecryptionMaterials", {})) + + assert materials.is_complete + + +@pytest.mark.parametrize( + "mod_kwargs", + ( + dict(algorithm=_REMOVE), + dict(encryption_context=_REMOVE), + dict(data_encryption_key=_REMOVE, data_key=_REMOVE), + dict(verification_key=_REMOVE), + ), +) +def test_decryption_materials_is_not_complete(mod_kwargs): + kwargs = _copy_and_update_kwargs("DecryptionMaterials", mod_kwargs) + materials = DecryptionMaterials(**kwargs) + + assert not materials.is_complete + + +def test_encryption_materials_is_complete(): + materials = EncryptionMaterials( + **_copy_and_update_kwargs("EncryptionMaterials", dict(encrypted_data_keys=[_ENCRYPTED_DATA_KEY])) + ) + + assert materials.is_complete + + +@pytest.mark.parametrize( + "mod_kwargs", + ( + dict(data_encryption_key=_REMOVE, encrypted_data_keys=_REMOVE), + dict(encrypted_data_keys=[]), + dict(encrypted_data_keys=_REMOVE), + dict(signing_key=_REMOVE), + ), +) +def test_encryption_materials_is_not_complete(mod_kwargs): + kwargs = _copy_and_update_kwargs("EncryptionMaterials", mod_kwargs) + materials = EncryptionMaterials(**kwargs) + + assert not materials.is_complete diff --git a/test/unit/streaming_client/__init__.py b/test/unit/streaming_client/__init__.py new file mode 100644 index 000000000..ad0e71d6c --- /dev/null +++ b/test/unit/streaming_client/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Dummy stub to make linters work better.""" diff --git a/test/unit/test_streaming_client_configs.py b/test/unit/streaming_client/test_configs.py similarity index 98% rename from test/unit/test_streaming_client_configs.py rename to test/unit/streaming_client/test_configs.py index 98e5cb13c..a98a38957 100644 --- a/test/unit/test_streaming_client_configs.py +++ b/test/unit/streaming_client/test_configs.py @@ -22,7 +22,7 @@ from aws_encryption_sdk.materials_managers.default import DefaultCryptoMaterialsManager from aws_encryption_sdk.streaming_client import DecryptorConfig, EncryptorConfig, _ClientConfig -from .unit_test_utils import all_invalid_kwargs, all_valid_kwargs, build_valid_kwargs_list +from ..unit_test_utils import all_invalid_kwargs, all_valid_kwargs, build_valid_kwargs_list pytestmark = [pytest.mark.unit, pytest.mark.local] diff --git a/test/unit/test_streaming_client_encryption_stream.py b/test/unit/streaming_client/test_encryption_stream.py similarity index 99% rename from test/unit/test_streaming_client_encryption_stream.py rename to test/unit/streaming_client/test_encryption_stream.py index e3a06347a..345e9939e 100644 --- a/test/unit/test_streaming_client_encryption_stream.py +++ b/test/unit/streaming_client/test_encryption_stream.py @@ -23,8 +23,8 @@ from aws_encryption_sdk.key_providers.base import MasterKeyProvider from aws_encryption_sdk.streaming_client import _ClientConfig, _EncryptionStream -from .test_values import VALUES -from .unit_test_utils import assert_prepped_stream_identity +from ..unit_test_utils import assert_prepped_stream_identity +from ..vectors import VALUES pytestmark = [pytest.mark.unit, pytest.mark.local] diff --git a/test/unit/test_streaming_client_stream_decryptor.py b/test/unit/streaming_client/test_stream_decryptor.py similarity index 99% rename from test/unit/test_streaming_client_stream_decryptor.py rename to test/unit/streaming_client/test_stream_decryptor.py index 6a3ccb56d..06e7f0816 100644 --- a/test/unit/test_streaming_client_stream_decryptor.py +++ b/test/unit/streaming_client/test_stream_decryptor.py @@ -22,7 +22,7 @@ from aws_encryption_sdk.materials_managers.base import CryptoMaterialsManager from aws_encryption_sdk.streaming_client import StreamDecryptor -from .test_values import VALUES +from ..vectors import VALUES pytestmark = [pytest.mark.unit, pytest.mark.local] diff --git a/test/unit/test_streaming_client_stream_encryptor.py b/test/unit/streaming_client/test_stream_encryptor.py similarity index 99% rename from test/unit/test_streaming_client_stream_encryptor.py rename to test/unit/streaming_client/test_stream_encryptor.py index 501214e9f..42516444a 100644 --- a/test/unit/test_streaming_client_stream_encryptor.py +++ b/test/unit/streaming_client/test_stream_encryptor.py @@ -30,7 +30,7 @@ from aws_encryption_sdk.streaming_client import StreamEncryptor from aws_encryption_sdk.structures import MessageHeader -from .test_values import VALUES +from ..vectors import VALUES pytestmark = [pytest.mark.unit, pytest.mark.local] @@ -247,7 +247,7 @@ def test_prep_message_framed_message( encryption_context=VALUES["encryption_context"], ) test_encryptor.content_type = ContentType.FRAMED_DATA - test_encryption_context = {aws_encryption_sdk.internal.defaults.ENCODED_SIGNER_KEY: sentinel.decoded_bytes} + test_encryption_context = {aws_encryption_sdk.internal.defaults.ENCODED_SIGNER_KEY: "DECODED_BYTES"} self.mock_encryption_materials.encryption_context = test_encryption_context self.mock_encryption_materials.encrypted_data_keys = self.mock_encrypted_data_keys diff --git a/test/unit/test_aws_encryption_sdk.py b/test/unit/test_client.py similarity index 89% rename from test/unit/test_aws_encryption_sdk.py rename to test/unit/test_client.py index 38dfff85a..d6b763b49 100644 --- a/test/unit/test_aws_encryption_sdk.py +++ b/test/unit/test_client.py @@ -17,7 +17,12 @@ import aws_encryption_sdk import aws_encryption_sdk.internal.defaults +from .vectors import VALUES + pytestmark = [pytest.mark.unit, pytest.mark.local] +_CIPHERTEXT = b"CIPHERTEXT" +_PLAINTEXT = b"PLAINTEXT" +_HEADER = VALUES["deserialized_header_frame"] class TestAwsEncryptionSdk(object): @@ -27,16 +32,16 @@ def apply_fixtures(self): self.mock_stream_encryptor_patcher = patch("aws_encryption_sdk.StreamEncryptor") self.mock_stream_encryptor = self.mock_stream_encryptor_patcher.start() self.mock_stream_encryptor_instance = MagicMock() - self.mock_stream_encryptor_instance.read.return_value = sentinel.ciphertext - self.mock_stream_encryptor_instance.header = sentinel.header + self.mock_stream_encryptor_instance.read.return_value = _CIPHERTEXT + self.mock_stream_encryptor_instance.header = _HEADER self.mock_stream_encryptor.return_value = self.mock_stream_encryptor_instance self.mock_stream_encryptor_instance.__enter__.return_value = self.mock_stream_encryptor_instance # Set up StreamDecryptor patch self.mock_stream_decryptor_patcher = patch("aws_encryption_sdk.StreamDecryptor") self.mock_stream_decryptor = self.mock_stream_decryptor_patcher.start() self.mock_stream_decryptor_instance = MagicMock() - self.mock_stream_decryptor_instance.read.return_value = sentinel.plaintext - self.mock_stream_decryptor_instance.header = sentinel.header + self.mock_stream_decryptor_instance.read.return_value = _PLAINTEXT + self.mock_stream_decryptor_instance.header = _HEADER self.mock_stream_decryptor.return_value = self.mock_stream_decryptor_instance self.mock_stream_decryptor_instance.__enter__.return_value = self.mock_stream_decryptor_instance yield @@ -47,14 +52,14 @@ def apply_fixtures(self): def test_encrypt(self): test_ciphertext, test_header = aws_encryption_sdk.encrypt(a=sentinel.a, b=sentinel.b, c=sentinel.b) self.mock_stream_encryptor.called_once_with(a=sentinel.a, b=sentinel.b, c=sentinel.b) - assert test_ciphertext is sentinel.ciphertext - assert test_header is sentinel.header + assert test_ciphertext is _CIPHERTEXT + assert test_header is _HEADER def test_decrypt(self): test_plaintext, test_header = aws_encryption_sdk.decrypt(a=sentinel.a, b=sentinel.b, c=sentinel.b) self.mock_stream_encryptor.called_once_with(a=sentinel.a, b=sentinel.b, c=sentinel.b) - assert test_plaintext is sentinel.plaintext - assert test_header is sentinel.header + assert test_plaintext is _PLAINTEXT + assert test_header is _HEADER def test_stream_encryptor_e(self): test = aws_encryption_sdk.stream(mode="e", a=sentinel.a, b=sentinel.b, c=sentinel.b) diff --git a/test/unit/test_material_managers.py b/test/unit/test_material_managers.py deleted file mode 100644 index fcd4977f5..000000000 --- a/test/unit/test_material_managers.py +++ /dev/null @@ -1,98 +0,0 @@ -# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). You -# may not use this file except in compliance with the License. A copy of -# the License is located at -# -# http://aws.amazon.com/apache2.0/ -# -# or in the "license" file accompanying this file. This file is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF -# ANY KIND, either express or implied. See the License for the specific -# language governing permissions and limitations under the License. -"""Test suite for aws_encryption_sdk.materials_managers""" -import pytest -from mock import MagicMock -from pytest_mock import mocker # noqa pylint: disable=unused-import - -from aws_encryption_sdk.identifiers import Algorithm -from aws_encryption_sdk.internal.utils.streams import ROStream -from aws_encryption_sdk.materials_managers import ( - DecryptionMaterials, - DecryptionMaterialsRequest, - EncryptionMaterials, - EncryptionMaterialsRequest, -) -from aws_encryption_sdk.structures import DataKey - -pytestmark = [pytest.mark.unit, pytest.mark.local] - - -_VALID_KWARGS = { - "EncryptionMaterialsRequest": dict( - encryption_context={}, - plaintext_rostream=MagicMock(__class__=ROStream), - frame_length=5, - algorithm=MagicMock(__class__=Algorithm), - plaintext_length=5, - ), - "EncryptionMaterials": dict( - algorithm=MagicMock(__class__=Algorithm), - data_encryption_key=MagicMock(__class__=DataKey), - encrypted_data_keys=set([]), - encryption_context={}, - signing_key=b"", - ), - "DecryptionMaterialsRequest": dict( - algorithm=MagicMock(__class__=Algorithm), encrypted_data_keys=set([]), encryption_context={} - ), - "DecryptionMaterials": dict(data_key=MagicMock(__class__=DataKey), verification_key=b"ex_verification_key"), -} - - -@pytest.mark.parametrize( - "attr_class, invalid_kwargs", - ( - (EncryptionMaterialsRequest, dict(encryption_context=None)), - (EncryptionMaterialsRequest, dict(frame_length="not an int")), - (EncryptionMaterialsRequest, dict(algorithm="not an Algorithm or None")), - (EncryptionMaterialsRequest, dict(plaintext_length="not an int or None")), - (EncryptionMaterials, dict(algorithm=None)), - (EncryptionMaterials, dict(data_encryption_key=None)), - (EncryptionMaterials, dict(encrypted_data_keys=None)), - (EncryptionMaterials, dict(encryption_context=None)), - (EncryptionMaterials, dict(signing_key=u"not bytes or None")), - (DecryptionMaterialsRequest, dict(algorithm=None)), - (DecryptionMaterialsRequest, dict(encrypted_data_keys=None)), - (DecryptionMaterialsRequest, dict(encryption_context=None)), - (DecryptionMaterials, dict(data_key=None)), - (DecryptionMaterials, dict(verification_key=5555)), - ), -) -def test_attributes_fails(attr_class, invalid_kwargs): - kwargs = _VALID_KWARGS[attr_class.__name__].copy() - kwargs.update(invalid_kwargs) - with pytest.raises(TypeError): - attr_class(**kwargs) - - -def test_encryption_materials_request_attributes_defaults(): - test = EncryptionMaterialsRequest(encryption_context={}, frame_length=5) - assert test.plaintext_rostream is None - assert test.algorithm is None - assert test.plaintext_length is None - - -def test_encryption_materials_defaults(): - test = EncryptionMaterials( - algorithm=MagicMock(__class__=Algorithm), - data_encryption_key=MagicMock(__class__=DataKey), - encrypted_data_keys=set([]), - encryption_context={}, - ) - assert test.signing_key is None - - -def test_decryption_materials_defaults(): - test = DecryptionMaterials(data_key=MagicMock(__class__=DataKey)) - assert test.verification_key is None diff --git a/test/unit/test_structures.py b/test/unit/test_structures.py index 1a9caa01d..26cef17ec 100644 --- a/test/unit/test_structures.py +++ b/test/unit/test_structures.py @@ -13,8 +13,16 @@ """Unit test suite for aws_encryption_sdk.structures""" import pytest -from aws_encryption_sdk.identifiers import Algorithm, ContentType, ObjectType, SerializationVersion -from aws_encryption_sdk.structures import DataKey, EncryptedDataKey, MasterKeyInfo, MessageHeader, RawDataKey +from aws_encryption_sdk.identifiers import Algorithm, ContentType, KeyringTraceFlag, ObjectType, SerializationVersion +from aws_encryption_sdk.structures import ( + CryptoResult, + DataKey, + EncryptedDataKey, + KeyringTrace, + MasterKeyInfo, + MessageHeader, + RawDataKey, +) from .unit_test_utils import all_invalid_kwargs, all_valid_kwargs @@ -57,6 +65,34 @@ key_provider=MasterKeyInfo(provider_id="asjnoa", key_info=b"aosjfoaiwej"), encrypted_data_key=b"aisofiawjef" ) ], + KeyringTrace: [ + dict( + wrapping_key=MasterKeyInfo(provider_id="foo", key_info=b"bar"), flags={KeyringTraceFlag.ENCRYPTED_DATA_KEY}, + ) + ], + CryptoResult: [ + dict( + result=b"super secret stuff", + header=MessageHeader( + version=SerializationVersion.V1, + type=ObjectType.CUSTOMER_AE_DATA, + algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + message_id=b"aosiejfoaiwej", + encryption_context={}, + encrypted_data_keys=set([]), + content_type=ContentType.FRAMED_DATA, + content_aad_length=32456, + header_iv_length=32456, + frame_length=234567, + ), + keyring_trace=( + KeyringTrace( + wrapping_key=MasterKeyInfo(provider_id="foo", key_info=b"bar"), + flags={KeyringTraceFlag.ENCRYPTED_DATA_KEY}, + ), + ), + ) + ], } @@ -107,3 +143,60 @@ def test_data_key_repr_str(cls, params): assert data_key_check not in str(test) assert data_key_check not in repr(test) + + +@pytest.fixture +def ex_data_key(): + return DataKey(**VALID_KWARGS[DataKey][0]) + + +def test_encrypted_data_key_from_data_key_success(ex_data_key): + test = EncryptedDataKey.from_data_key(ex_data_key) + + assert test.key_provider == ex_data_key.key_provider + assert test.encrypted_data_key == ex_data_key.encrypted_data_key + + +def test_raw_data_key_from_data_key_success(ex_data_key): + test = RawDataKey.from_data_key(ex_data_key) + + assert test.key_provider == ex_data_key.key_provider + assert test.data_key == ex_data_key.data_key + + +@pytest.mark.parametrize("data_key_class", (EncryptedDataKey, RawDataKey)) +def test_raw_and_encrypted_data_key_from_data_key_fail(data_key_class): + with pytest.raises(TypeError) as excinfo: + data_key_class.from_data_key(b"ahjseofij") + + excinfo.match(r"data_key must be type DataKey not *") + + +@pytest.fixture +def ex_result(): + return CryptoResult(**VALID_KWARGS[CryptoResult][0]) + + +def test_cryptoresult_len(ex_result): + assert len(ex_result) == 2 + + +def test_cryptoresult_unpack(ex_result): + data, header = ex_result + + assert data is ex_result.result + assert header is ex_result.header + + +def test_cryptoresult_getitem(ex_result): + data = ex_result[0] + header = ex_result[1] + + assert data is ex_result.result + assert header is ex_result.header + + +def test_cryptoresult_to_tuple(ex_result): + test = tuple(ex_result) + + assert test == ex_result._legacy_container diff --git a/test/unit/unit_test_utils.py b/test/unit/unit_test_utils.py index 6b0a84bdc..d175f0b3e 100644 --- a/test/unit/unit_test_utils.py +++ b/test/unit/unit_test_utils.py @@ -1,21 +1,310 @@ -# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). You -# may not use this file except in compliance with the License. A copy of -# the License is located at -# -# http://aws.amazon.com/apache2.0/ -# -# or in the "license" file accompanying this file. This file is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF -# ANY KIND, either express or implied. See the License for the specific -# language governing permissions and limitations under the License. +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 """Utility functions to handle common test framework functions.""" +import base64 import copy import io import itertools +import os +import attr +from attr.validators import instance_of +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import rsa + +from aws_encryption_sdk.exceptions import DecryptKeyError +from aws_encryption_sdk.identifiers import AlgorithmSuite, EncryptionKeyType, KeyringTraceFlag, WrappingAlgorithm +from aws_encryption_sdk.internal.crypto.wrapping_keys import WrappingKey from aws_encryption_sdk.internal.utils.streams import InsistentReaderBytesIO +from aws_encryption_sdk.key_providers.base import MasterKeyProvider, MasterKeyProviderConfig +from aws_encryption_sdk.key_providers.raw import RawMasterKey, RawMasterKeyProvider +from aws_encryption_sdk.keyrings.base import Keyring +from aws_encryption_sdk.keyrings.multi import MultiKeyring +from aws_encryption_sdk.keyrings.raw import RawAESKeyring, RawRSAKeyring +from aws_encryption_sdk.materials_managers import DecryptionMaterials, EncryptionMaterials +from aws_encryption_sdk.structures import EncryptedDataKey, KeyringTrace, MasterKeyInfo, RawDataKey + +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Dict, Iterable, Optional # noqa pylint: disable=unused-import +except ImportError: # pragma: no cover + # We only actually need these imports when running the mypy checks + pass + +_ENCRYPTION_CONTEXT = {"encryption": "context", "values": "here"} +_PROVIDER_ID = "Random Raw Keys" +_EXISTING_KEY_ID = b"pre-seeded key id" +_KEY_ID = b"5325b043-5843-4629-869c-64794af77ada" +_WRAPPING_KEY = b"\xeby-\x80A6\x15rA8\x83#,\xe4\xab\xac`\xaf\x99Z\xc1\xce\xdb\xb6\x0f\xb7\x805\xb2\x14J3" +_SIGNING_KEY = b"aws-crypto-public-key" +_DATA_KEY = ( + b"\x00\xfa\x8c\xdd\x08Au\xc6\x92_4\xc5\xfb\x90\xaf\x8f\xa1D\xaf\xcc\xd25" b"\xa8\x0b\x0b\x16\x92\x91W\x01\xb7\x84" +) +_WRAPPING_KEY_AES = b"\xeby-\x80A6\x15rA8\x83#,\xe4\xab\xac`\xaf\x99Z\xc1\xce\xdb\xb6\x0f\xb7\x805\xb2\x14J3" + +_PUBLIC_EXPONENT = 65537 +_KEY_SIZE = 2048 +_BACKEND = default_backend() + +_ENCRYPTED_DATA_KEY_AES = EncryptedDataKey( + key_provider=MasterKeyInfo( + provider_id="Random Raw Keys", + key_info=b"5325b043-5843-4629-869c-64794af77ada\x00\x00\x00\x80" + b"\x00\x00\x00\x0c\xc7\xd5d\xc9\xc5\xf21\x8d\x8b\xf9H" + b"\xbb", + ), + encrypted_data_key=b"\xf3+\x15n\xe6`\xbe\xfe\xf0\x9e1\xe5\x9b" + b"\xaf\xfe\xdaT\xbb\x17\x14\xfd} o\xdd\xf1" + b"\xbc\xe1C\xa5J\xd8\xc7\x15\xc2\x90t=\xb9" + b"\xfd;\x94lTu/6\xfe", +) + +_ENCRYPTED_DATA_KEY_NOT_IN_KEYRING = EncryptedDataKey( + key_provider=MasterKeyInfo( + provider_id="Random Raw Keys", + key_info=b"5430b043-5843-4629-869c-64794af77ada\x00\x00\x00\x80" + b"\x00\x00\x00\x0c\xc7\xd5d\xc9\xc5\xf21\x8d\x8b\xf9H" + b"\xbb", + ), + encrypted_data_key=b"\xf3+\x15n\xe6`\xbe\xfe\xf0\x9e1\xe5\x9b" + b"\xaf\xfe\xdaT\xbb\x17\x14\xfd} o\xdd\xf1" + b"\xbc\xe1C\xa5J\xd8\xc7\x15\xc2\x90t=\xb9" + b"\xfd;\x94lTu/6\xfe", +) + +_ENCRYPTED_DATA_KEY_RSA = EncryptedDataKey( + key_provider=MasterKeyInfo(provider_id="Random Raw Keys", key_info=_KEY_ID), + encrypted_data_key=b"\xf3+\x15n\xe6`\xbe\xfe\xf0\x9e1\xe5\x9b" + b"\xaf\xfe\xdaT\xbb\x17\x14\xfd} o\xdd\xf1" + b"\xbc\xe1C\xa5J\xd8\xc7\x15\xc2\x90t=\xb9" + b"\xfd;\x94lTu/6\xfe", +) + + +class IdentityKeyring(Keyring): + def on_encrypt(self, encryption_materials): + # type: (EncryptionMaterials) -> EncryptionMaterials + return encryption_materials + + def on_decrypt(self, decryption_materials, encrypted_data_keys): + # type: (DecryptionMaterials, Iterable[EncryptedDataKey]) -> DecryptionMaterials + return decryption_materials + + +class OnlyGenerateKeyring(Keyring): + def on_encrypt(self, encryption_materials): + # type: (EncryptionMaterials) -> EncryptionMaterials + if encryption_materials.data_encryption_key is None: + key_provider = MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID) + data_encryption_key = RawDataKey( + key_provider=key_provider, data_key=os.urandom(encryption_materials.algorithm.kdf_input_len) + ) + encryption_materials = encryption_materials.with_data_encryption_key( + data_encryption_key=data_encryption_key, + keyring_trace=KeyringTrace(wrapping_key=key_provider, flags={KeyringTraceFlag.GENERATED_DATA_KEY}), + ) + return encryption_materials + + def on_decrypt(self, decryption_materials, encrypted_data_keys): + # type: (DecryptionMaterials, Iterable[EncryptedDataKey]) -> DecryptionMaterials + return decryption_materials + + +def get_encryption_materials_with_data_key(): + return EncryptionMaterials( + algorithm=AlgorithmSuite.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + data_encryption_key=RawDataKey( + key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_EXISTING_KEY_ID), + data_key=b'*!\xa1"^-(\xf3\x105\x05i@B\xc2\xa2\xb7\xdd\xd5\xd5\xa9\xddm\xfae\xa8\\$\xf9d\x1e(', + ), + encryption_context=_ENCRYPTION_CONTEXT, + signing_key=_SIGNING_KEY, + keyring_trace=[ + KeyringTrace( + wrapping_key=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_EXISTING_KEY_ID), + flags={KeyringTraceFlag.GENERATED_DATA_KEY}, + ) + ], + ) + + +def get_encryption_materials_with_data_encryption_key(): + return EncryptionMaterials( + algorithm=AlgorithmSuite.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + data_encryption_key=RawDataKey( + key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_EXISTING_KEY_ID), + data_key=b'*!\xa1"^-(\xf3\x105\x05i@B\xc2\xa2\xb7\xdd\xd5\xd5\xa9\xddm\xfae\xa8\\$\xf9d\x1e(', + ), + encryption_context=_ENCRYPTION_CONTEXT, + signing_key=_SIGNING_KEY, + keyring_trace=[ + KeyringTrace( + wrapping_key=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_EXISTING_KEY_ID), + flags={KeyringTraceFlag.GENERATED_DATA_KEY}, + ) + ], + ) + + +def get_encryption_materials_without_data_key(): + return EncryptionMaterials( + algorithm=AlgorithmSuite.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + encryption_context=_ENCRYPTION_CONTEXT, + signing_key=_SIGNING_KEY, + ) + + +def get_encryption_materials_with_encrypted_data_key(): + return EncryptionMaterials( + algorithm=AlgorithmSuite.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + data_encryption_key=RawDataKey( + key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_EXISTING_KEY_ID), + data_key=b'*!\xa1"^-(\xf3\x105\x05i@B\xc2\xa2\xb7\xdd\xd5\xd5\xa9\xddm\xfae\xa8\\$\xf9d\x1e(', + ), + encrypted_data_keys=[ + EncryptedDataKey( + key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_EXISTING_KEY_ID), + encrypted_data_key=b"\xde^\x97\x7f\x84\xe9\x9e\x98\xd0\xe2\xf8\xd5\xcb\xe9\x7f.}\x87\x16,\x11n#\xc8p" + b"\xdb\xbf\x94\x86*Q\x06\xd2\xf5\xdah\x08\xa4p\x81\xf7\xf4G\x07FzE\xde", + ) + ], + encryption_context=_ENCRYPTION_CONTEXT, + signing_key=_SIGNING_KEY, + keyring_trace=[ + KeyringTrace( + wrapping_key=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_EXISTING_KEY_ID), + flags={KeyringTraceFlag.GENERATED_DATA_KEY, KeyringTraceFlag.ENCRYPTED_DATA_KEY}, + ) + ], + ) + + +def get_encryption_materials_with_encrypted_data_key_aes(): + return EncryptionMaterials( + algorithm=AlgorithmSuite.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + data_encryption_key=RawDataKey( + key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_EXISTING_KEY_ID), + data_key=b'*!\xa1"^-(\xf3\x105\x05i@B\xc2\xa2\xb7\xdd\xd5\xd5\xa9\xddm\xfae\xa8\\$\xf9d\x1e(', + ), + encrypted_data_keys=[_ENCRYPTED_DATA_KEY_AES], + encryption_context=_ENCRYPTION_CONTEXT, + signing_key=_SIGNING_KEY, + keyring_trace=[ + KeyringTrace( + wrapping_key=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_EXISTING_KEY_ID), + flags={KeyringTraceFlag.GENERATED_DATA_KEY, KeyringTraceFlag.ENCRYPTED_DATA_KEY}, + ) + ], + ) + + +def get_encryption_materials_without_data_encryption_key(): + return EncryptionMaterials( + algorithm=AlgorithmSuite.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + encryption_context=_ENCRYPTION_CONTEXT, + signing_key=_SIGNING_KEY, + ) + + +def get_decryption_materials_without_data_encryption_key(): + return DecryptionMaterials( + algorithm=AlgorithmSuite.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + verification_key=b"ex_verification_key", + encryption_context=_ENCRYPTION_CONTEXT, + ) + + +def get_decryption_materials_with_data_key(): + return DecryptionMaterials( + algorithm=AlgorithmSuite.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + data_encryption_key=RawDataKey( + key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_EXISTING_KEY_ID), + data_key=b'*!\xa1"^-(\xf3\x105\x05i@B\xc2\xa2\xb7\xdd\xd5\xd5\xa9\xddm\xfae\xa8\\$\xf9d\x1e(', + ), + encryption_context=_ENCRYPTION_CONTEXT, + verification_key=b"ex_verification_key", + keyring_trace=[ + KeyringTrace( + wrapping_key=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_EXISTING_KEY_ID), + flags={KeyringTraceFlag.DECRYPTED_DATA_KEY}, + ) + ], + ) + + +def get_decryption_materials_with_data_encryption_key(): + return DecryptionMaterials( + algorithm=AlgorithmSuite.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + data_encryption_key=RawDataKey( + key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_EXISTING_KEY_ID), + data_key=b'*!\xa1"^-(\xf3\x105\x05i@B\xc2\xa2\xb7\xdd\xd5\xd5\xa9\xddm\xfae\xa8\\$\xf9d\x1e(', + ), + encryption_context=_ENCRYPTION_CONTEXT, + verification_key=b"ex_verification_key", + keyring_trace=[ + KeyringTrace( + wrapping_key=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_EXISTING_KEY_ID), + flags={KeyringTraceFlag.DECRYPTED_DATA_KEY}, + ) + ], + ) + + +def get_decryption_materials_without_data_key(): + return DecryptionMaterials(encryption_context=_ENCRYPTION_CONTEXT, verification_key=b"ex_verification_key") + + +def get_multi_keyring_with_generator_and_children(): + return MultiKeyring( + generator=RawAESKeyring(key_namespace=_PROVIDER_ID, key_name=_KEY_ID, wrapping_key=_WRAPPING_KEY_AES,), + children=[ + RawRSAKeyring( + key_namespace=_PROVIDER_ID, + key_name=_KEY_ID, + wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, + private_wrapping_key=rsa.generate_private_key( + public_exponent=65537, key_size=2048, backend=default_backend() + ), + ), + RawRSAKeyring( + key_namespace=_PROVIDER_ID, + key_name=_KEY_ID, + wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, + private_wrapping_key=rsa.generate_private_key( + public_exponent=65537, key_size=2048, backend=default_backend() + ), + ), + ], + ) + + +def get_multi_keyring_with_no_children(): + return MultiKeyring( + generator=RawRSAKeyring( + key_namespace=_PROVIDER_ID, + key_name=_KEY_ID, + wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, + private_wrapping_key=rsa.generate_private_key( + public_exponent=65537, key_size=2048, backend=default_backend() + ), + ) + ) + + +def get_multi_keyring_with_no_generator(): + return MultiKeyring( + children=[ + RawRSAKeyring( + key_namespace=_PROVIDER_ID, + key_name=_KEY_ID, + wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, + private_wrapping_key=rsa.generate_private_key( + public_exponent=65537, key_size=2048, backend=default_backend() + ), + ), + RawAESKeyring(key_namespace=_PROVIDER_ID, key_name=_KEY_ID, wrapping_key=_WRAPPING_KEY_AES,), + ] + ) def all_valid_kwargs(valid_kwargs): @@ -93,3 +382,252 @@ def assert_prepped_stream_identity(prepped_stream, wrapped_type): assert isinstance(prepped_stream, wrapped_type) # Check the wrapping streams assert isinstance(prepped_stream, InsistentReaderBytesIO) + + +def _generate_rsa_key_bytes(size): + # type: (int) -> bytes + private_key = rsa.generate_private_key(public_exponent=65537, key_size=size, backend=default_backend()) + return private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ) + + +def ephemeral_raw_rsa_master_key(size=4096): + # type: (int) -> RawMasterKey + key_bytes = _generate_rsa_key_bytes(size) + return RawMasterKey( + provider_id="fake", + key_id="rsa-{}".format(size).encode("utf-8"), + wrapping_key=WrappingKey( + wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, + wrapping_key=key_bytes, + wrapping_key_type=EncryptionKeyType.PRIVATE, + ), + ) + + +def ephemeral_raw_rsa_keyring(size=4096, wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1): + # type: (int, WrappingAlgorithm) -> RawRSAKeyring + key_bytes = _generate_rsa_key_bytes(size) + return RawRSAKeyring.from_pem_encoding( + key_namespace="fake", + key_name="rsa-{}".format(size).encode("utf-8"), + wrapping_algorithm=wrapping_algorithm, + private_encoded_key=key_bytes, + ) + + +def raw_rsa_mkps_from_keyring(keyring): + # type: (RawRSAKeyring) -> (MasterKeyProvider, MasterKeyProvider) + """Constructs a private and public raw RSA MKP using the private key in the raw RSA keyring.""" + private_key = keyring._private_wrapping_key + private_pem = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ) + public_pem = private_key.public_key().public_bytes( + encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo + ) + private_key_mkp = RawMasterKey( + provider_id=keyring.key_namespace, + key_id=keyring.key_name, + wrapping_key=WrappingKey( + wrapping_algorithm=keyring._wrapping_algorithm, + wrapping_key=private_pem, + wrapping_key_type=EncryptionKeyType.PRIVATE, + ), + ) + public_key_mkp = RawMasterKey( + provider_id=keyring.key_namespace, + key_id=keyring.key_name, + wrapping_key=WrappingKey( + wrapping_algorithm=keyring._wrapping_algorithm, + wrapping_key=public_pem, + wrapping_key_type=EncryptionKeyType.PUBLIC, + ), + ) + return private_key_mkp, public_key_mkp + + +def ephemeral_raw_aes_master_key(wrapping_algorithm=WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING, key=None): + # type: (WrappingAlgorithm, Optional[bytes]) -> RawMasterKey + key_length = wrapping_algorithm.algorithm.data_key_len + if key is None: + key = os.urandom(key_length) + return RawMasterKey( + provider_id="fake", + key_id="aes-{}".format(key_length * 8).encode("utf-8"), + wrapping_key=WrappingKey( + wrapping_algorithm=wrapping_algorithm, wrapping_key=key, wrapping_key_type=EncryptionKeyType.SYMMETRIC, + ), + ) + + +def ephemeral_raw_aes_keyring(wrapping_algorithm=WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING, key=None): + # type: (WrappingAlgorithm, Optional[bytes]) -> RawAESKeyring + key_length = wrapping_algorithm.algorithm.data_key_len + if key is None: + key = os.urandom(key_length) + return RawAESKeyring( + key_namespace="fake", key_name="aes-{}".format(key_length * 8).encode("utf-8"), wrapping_key=key, + ) + + +class EphemeralRawMasterKeyProvider(RawMasterKeyProvider): + """Master key provider with raw master keys that are generated on each initialization.""" + + provider_id = "fake" + + def __init__(self): + self.__keys = { + b"aes-256": ephemeral_raw_aes_master_key(WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING), + b"rsa-4096": ephemeral_raw_rsa_master_key(4096), + } + + def _get_raw_key(self, key_id): + return self.__keys[key_id].config.wrapping_key + + +class EmptyMasterKeyProvider(MasterKeyProvider): + """Master key provider that provides no master keys.""" + + provider_id = "empty" + _config_class = MasterKeyProviderConfig + vend_masterkey_on_decrypt = False + + def _new_master_key(self, key_id): + raise Exception("How did this happen??") + + def master_keys_for_encryption(self, encryption_context, plaintext_rostream, plaintext_length=None): + return ephemeral_raw_aes_master_key(), [] + + +class DisjointMasterKeyProvider(MasterKeyProvider): + """Master key provider that does not provide the primary master key in the additional master keys.""" + + provider_id = "disjoint" + _config_class = MasterKeyProviderConfig + vend_masterkey_on_decrypt = False + + def _new_master_key(self, key_id): + raise Exception("How did this happen??") + + def master_keys_for_encryption(self, encryption_context, plaintext_rostream, plaintext_length=None): + return ephemeral_raw_aes_master_key(), [ephemeral_raw_rsa_master_key()] + + +class FailingDecryptMasterKeyProvider(EphemeralRawMasterKeyProvider): + """EphemeralRawMasterKeyProvider that cannot decrypt.""" + + def decrypt_data_key(self, encrypted_data_key, algorithm, encryption_context): + raise DecryptKeyError("FailingDecryptMasterKeyProvider cannot decrypt!") + + +@attr.s +class BrokenKeyring(Keyring): + """Keyring that wraps another keyring and selectively breaks the returned values.""" + + _inner_keyring = attr.ib(validator=instance_of(Keyring)) + _break_algorithm = attr.ib(default=False, validator=instance_of(bool)) + _break_encryption_context = attr.ib(default=False, validator=instance_of(bool)) + _break_signing = attr.ib(default=False, validator=instance_of(bool)) + + @staticmethod + def _random_string(bytes_len): + # type: (int) -> str + return base64.b64encode(os.urandom(bytes_len)).decode("utf-8") + + def _broken_algorithm(self, algorithm): + # type: (AlgorithmSuite) -> AlgorithmSuite + if not self._break_algorithm: + return algorithm + + # We want to make sure that we return something different, + # so find this suite in all suites and grab the next one, + # whatever that is. + all_suites = list(AlgorithmSuite) + suite_index = all_suites.index(algorithm) + next_index = (suite_index + 1) % (len(all_suites) - 1) + + return all_suites[next_index] + + def _broken_encryption_context(self, encryption_context): + # type: (Dict[str, str]) -> Dict[str, str] + broken_ec = encryption_context.copy() + + if not self._break_encryption_context: + return broken_ec + + # Remove a random value + try: + broken_ec.popitem() + except KeyError: + pass + + # add a random value + broken_ec[self._random_string(5)] = self._random_string(10) + + return broken_ec + + def _broken_key(self, key): + # type: (bytes) -> bytes + if not self._break_signing: + return key + + return self._random_string(32).encode("utf-8") + + def _break_encryption_materials(self, encryption_materials): + # type: (EncryptionMaterials) -> EncryptionMaterials + return EncryptionMaterials( + algorithm=self._broken_algorithm(encryption_materials.algorithm), + data_encryption_key=encryption_materials.data_encryption_key, + encrypted_data_keys=encryption_materials.encrypted_data_keys, + encryption_context=self._broken_encryption_context(encryption_materials.encryption_context), + signing_key=self._broken_key(encryption_materials.signing_key), + keyring_trace=encryption_materials.keyring_trace, + ) + + def _break_decryption_materials(self, decryption_materials): + # type: (DecryptionMaterials) -> DecryptionMaterials + return DecryptionMaterials( + algorithm=self._broken_algorithm(decryption_materials.algorithm), + data_encryption_key=decryption_materials.data_encryption_key, + encryption_context=self._broken_encryption_context(decryption_materials.encryption_context), + verification_key=self._broken_key(decryption_materials.verification_key), + keyring_trace=decryption_materials.keyring_trace, + ) + + def on_encrypt(self, encryption_materials): + # type: (EncryptionMaterials) -> EncryptionMaterials + return self._break_encryption_materials(self._inner_keyring.on_encrypt(encryption_materials)) + + def on_decrypt(self, decryption_materials, encrypted_data_keys): + # type: (DecryptionMaterials, Iterable[EncryptedDataKey]) -> DecryptionMaterials + return self._break_decryption_materials( + self._inner_keyring.on_decrypt(decryption_materials, encrypted_data_keys) + ) + + +@attr.s +class NoEncryptedDataKeysKeyring(Keyring): + """Keyring that wraps another keyring and removes any encrypted data keys.""" + + _inner_keyring = attr.ib(validator=instance_of(Keyring)) + + def on_encrypt(self, encryption_materials): + # type: (EncryptionMaterials) -> EncryptionMaterials + materials = self._inner_keyring.on_encrypt(encryption_materials) + return EncryptionMaterials( + algorithm=materials.algorithm, + data_encryption_key=materials.data_encryption_key, + encryption_context=materials.encryption_context, + signing_key=materials.signing_key, + keyring_trace=materials.keyring_trace, + ) + + def on_decrypt(self, decryption_materials, encrypted_data_keys): + # type: (DecryptionMaterials, Iterable[EncryptedDataKey]) -> DecryptionMaterials + return self._inner_keyring.on_decrypt(decryption_materials, encrypted_data_keys) diff --git a/test/unit/test_values.py b/test/unit/vectors.py similarity index 88% rename from test/unit/test_values.py rename to test/unit/vectors.py index 26eff1341..b9aba0c02 100644 --- a/test/unit/test_values.py +++ b/test/unit/vectors.py @@ -187,6 +187,55 @@ def array_byte(source): "\xff\x8fn\x95\xf0\xf0E\x91Uj\xb0E3=\x0e\x1a\xf1'4\xf6" ), "signature_len": b"\x00h", + "private_rsa_key_bytes": [ + ( + b"-----BEGIN RSA PRIVATE KEY-----" + b"MIICXgIBAAKBgQCUjhI8YRPXV8Gfofbg/" + b"PLjWw2AzowQTPErLU2z3+xGqElMdzdiC4Ta43DFWZg34Eg0X8kQPAeoe8h3cRSMo" + b"77eSOHt2dPo7OfTfZqsH8766fivHIKVxBYPX8SZYIUhMtRnlg3uqch9BksfRop+h" + b"f8h/H3lfervJoevS2CXYB9/iwIDAQABAoGBAIqeGzQOHbaGI51yQ2zjez1dPDdiB" + b"F49fZideHEM1GuGIodgguRQ/VJGgncUSC5zcMy2SGaGrVqwznltohAtxy4rZp0eh" + b"2O3aHYi9Wehd0SPLh+qwu7mJDuh0z15hmCOue070FnUtyuSwhXLwDrbot2+5HbmF" + b"9clJLI5tv92gvIpAkEA+Bv5i8XJNPN1rao31aQFoi9bFIOEclk3b1RbLX6mpZBFS" + b"U9CNUy0RQNC0+H3KZ5CTvsyFGpMfTdiFc/Qdesk3QJBAJlHjrvoadP+PU3zXYrWR" + b"D5EryyTxaP1bOjrp9xLuQBeU8x7EVJdpoul9OmwcT3NrAqvxDE9okjha2tjCI6O2" + b"4cCQQDMyOJPYL/zaaPO5LlTKB/SPv4RT4BplYPw6xKa2XeZHhxiJv5B2f7NG6T0G" + b"AWWn16hrCoouZhKngTidfXc7motAkA/KiTgvKr3yHp86AAxWZDv1CAYD6FPqrDB3" + b"3LiLnZDd5uy1ThTJ/Kc87vUnXhdDqeKE9qWrB53SCWbMElzbd17AkEA4DMp+6ngM" + b"o6sS0dY1X6nTLqgvK3B0z5GCAdSEy3Y8jh995Lrl+hy88HzuwUkQwwPlZkFhUNCx" + b"edrC6cTKE5xLA==" + b"-----END RSA PRIVATE KEY-----" + ), + ( + b"-----BEGIN RSA PRIVATE KEY-----\n" + b"MIIEowIBAAKCAQEAo8uCyhiO4JUGZV+rtNq5DBA9Lm4xkw5kTA3v6EPybs8bVXL2\n" + b"ZE6jkbo+xT4Jg/bKzUpnp1fE+T1ruGPtsPdoEmhY/P64LDNIs3sRq5U4QV9IETU1\n" + b"vIcbNNkgGhRjV8J87YNY0tV0H7tuWuZRpqnS+gjV6V9lUMkbvjMCc5IBqQc3heut\n" + b"/+fH4JwpGlGxOVXI8QAapnSy1XpCr3+PT29kydVJnIMuAoFrurojRpOQbOuVvhtA\n" + b"gARhst1Ji4nfROGYkj6eZhvkz2Bkud4/+3lGvVU5LO1vD8oY7WoGtpin3h50VcWe\n" + b"aBT4kejx4s9/G9C4R24lTH09J9HO2UUsuCqZYQIDAQABAoIBAQCfC90bCk+qaWqF\n" + b"gymC+qOWwCn4bM28gswHQb1D5r6AtKBRD8mKywVvWs7azguFVV3Fi8sspkBA2FBC\n" + b"At5p6ULoJOTL/TauzLl6djVJTCMM701WUDm2r+ZOIctXJ5bzP4n5Q4I7b0NMEL7u\n" + b"ixib4elYGr5D1vrVQAKtZHCr8gmkqyx8Mz7wkJepzBP9EeVzETCHsmiQDd5WYlO1\n" + b"C2IQYgw6MJzgM4entJ0V/GPytkodblGY95ORVK7ZhyNtda+r5BZ6/jeMW+hA3VoK\n" + b"tHSWjHt06ueVCCieZIATmYzBNt+zEz5UA2l7ksg3eWfVORJQS7a6Ef4VvbJLM9Ca\n" + b"m1kdsjelAoGBANKgvRf39i3bSuvm5VoyJuqinSb/23IH3Zo7XOZ5G164vh49E9Cq\n" + b"dOXXVxox74ppj/kbGUoOk+AvaB48zzfzNvac0a7lRHExykPH2kVrI/NwH/1OcT/x\n" + b"2e2DnFYocXcb4gbdZQ+m6X3zkxOYcONRzPVW1uMrFTWHcJveMUm4PGx7AoGBAMcU\n" + b"IRvrT6ye5se0s27gHnPweV+3xjsNtXZcK82N7duXyHmNjxrwOAv0SOhUmTkRXArM\n" + b"6aN5D8vyZBSWma2TgUKwpQYFTI+4Sp7sdkkyojGAEixJ+c5TZJNxZFrUe0FwAoic\n" + b"c2kb7ntaiEj5G+qHvykJJro5hy6uLnjiMVbAiJDTAoGAKb67241EmHAXGEwp9sdr\n" + b"2SMjnIAnQSF39UKAthkYqJxa6elXDQtLoeYdGE7/V+J2K3wIdhoPiuY6b4vD0iX9\n" + b"JcGM+WntN7YTjX2FsC588JmvbWfnoDHR7HYiPR1E58N597xXdFOzgUgORVr4PMWQ\n" + b"pqtwaZO3X2WZlvrhr+e46hMCgYBfdIdrm6jYXFjL6RkgUNZJQUTxYGzsY+ZemlNm\n" + b"fGdQo7a8kePMRuKY2MkcnXPaqTg49YgRmjq4z8CtHokRcWjJUWnPOTs8rmEZUshk\n" + b"0KJ0mbQdCFt/Uv0mtXgpFTkEZ3DPkDTGcV4oR4CRfOCl0/EU/A5VvL/U4i/mRo7h\n" + b"ye+xgQKBgD58b+9z+PR5LAJm1tZHIwb4tnyczP28PzwknxFd2qylR4ZNgvAUqGtU\n" + b"xvpUDpzMioz6zUH9YV43YNtt+5Xnzkqj+u9Mr27/H2v9XPwORGfwQ5XPwRJz/2oC\n" + b"EnPmP1SZoY9lXKUpQXHXSpDZ2rE2Klt3RHMUMHt8Zpy36E8Vwx8o\n" + b"-----END RSA PRIVATE KEY-----\n" + ), + ], } VALUES["updated_encryption_context"] = copy.deepcopy(VALUES["encryption_context"]) VALUES["updated_encryption_context"]["aws-crypto-public-key"] = VALUES["encoded_curve_point"] diff --git a/test/upstream-requirements-py27.txt b/test/upstream-requirements-py27.txt index 082a6198e..92e723b9c 100644 --- a/test/upstream-requirements-py27.txt +++ b/test/upstream-requirements-py27.txt @@ -1,30 +1,72 @@ -asn1crypto==0.24.0 atomicwrites==1.3.0 -attrs==19.1.0 -boto3==1.9.133 -botocore==1.12.133 -cffi==1.12.3 -coverage==4.5.3 -cryptography==2.6.1 -docutils==0.14 -enum34==1.1.6 +attrs==19.3.0 +aws-sam-translator==1.21.0 +aws-xray-sdk==2.4.3 +backports.ssl-match-hostname==3.7.0.1 +backports.tempfile==1.0 +backports.weakref==1.0.post1 +boto==2.49.0 +boto3==1.12.16 +botocore==1.15.16 +certifi==2019.11.28 +cffi==1.14.0 +cfn-lint==0.28.3 +chardet==3.0.4 +configparser==4.0.2 +contextlib2==0.6.0.post1 +cookies==2.2.1 +coverage==5.0.3 +cryptography==2.8 +docker==4.2.0 +docutils==0.15.2 +ecdsa==0.15 +enum34==1.1.9 funcsigs==1.0.2 -futures==3.2.0 -ipaddress==1.0.22 -jmespath==0.9.4 -mock==2.0.0 +functools32==3.2.3.post2 +future==0.18.2 +futures==3.3.0 +idna==2.8 +importlib-metadata==1.5.0 +importlib-resources==1.0.2 +ipaddress==1.0.23 +Jinja2==2.11.1 +jmespath==0.9.5 +jsondiff==1.1.2 +jsonpatch==1.25 +jsonpickle==1.3 +jsonpointer==2.0 +jsonschema==3.2.0 +MarkupSafe==1.1.1 +mock==3.0.5 more-itertools==5.0.0 -pathlib2==2.3.3 -pbr==5.1.3 -pluggy==0.9.0 -py==1.8.0 -pycparser==2.19 -pytest==4.4.1 -pytest-cov==2.6.1 -pytest-mock==1.10.4 -python-dateutil==2.8.0 -s3transfer==0.2.0 +moto==1.3.14 +packaging==20.3 +pathlib2==2.3.5 +pluggy==0.13.1 +py==1.8.1 +pyasn1==0.4.8 +pycparser==2.20 +pyparsing==2.4.6 +pyrsistent==0.15.7 +pytest==4.6.9 +pytest-cov==2.8.1 +pytest-mock==2.0.0 +python-dateutil==2.8.1 +python-jose==3.1.0 +pytz==2019.3 +PyYAML==5.3 +requests==2.23.0 +responses==0.10.12 +rsa==4.0 +s3transfer==0.3.3 scandir==1.10.0 -six==1.12.0 -urllib3==1.24.2 -wrapt==1.11.1 +six==1.14.0 +sshpubkeys==3.1.0 +typing==3.7.4.1 +urllib3==1.25.8 +wcwidth==0.1.8 +websocket-client==0.57.0 +Werkzeug==1.0.0 +wrapt==1.12.0 +xmltodict==0.12.0 +zipp==1.2.0 diff --git a/test/upstream-requirements-py37.txt b/test/upstream-requirements-py37.txt index 238746675..03a6eb36e 100644 --- a/test/upstream-requirements-py37.txt +++ b/test/upstream-requirements-py37.txt @@ -1,24 +1,56 @@ -asn1crypto==0.24.0 -atomicwrites==1.3.0 -attrs==19.1.0 -boto3==1.9.133 -botocore==1.12.133 -cffi==1.12.3 -coverage==4.5.3 -cryptography==2.6.1 -docutils==0.14 -jmespath==0.9.4 -mock==2.0.0 -more-itertools==7.0.0 -pbr==5.1.3 -pluggy==0.9.0 -py==1.8.0 -pycparser==2.19 -pytest==4.4.1 -pytest-cov==2.6.1 -pytest-mock==1.10.4 -python-dateutil==2.8.0 -s3transfer==0.2.0 -six==1.12.0 -urllib3==1.24.2 -wrapt==1.11.1 +attrs==19.3.0 +aws-sam-translator==1.21.0 +aws-xray-sdk==2.4.3 +boto==2.49.0 +boto3==1.12.16 +botocore==1.15.16 +certifi==2019.11.28 +cffi==1.14.0 +cfn-lint==0.28.3 +chardet==3.0.4 +coverage==5.0.3 +cryptography==2.8 +docker==4.2.0 +docutils==0.15.2 +ecdsa==0.15 +future==0.18.2 +idna==2.8 +importlib-metadata==1.5.0 +Jinja2==2.11.1 +jmespath==0.9.5 +jsondiff==1.1.2 +jsonpatch==1.25 +jsonpickle==1.3 +jsonpointer==2.0 +jsonschema==3.2.0 +MarkupSafe==1.1.1 +mock==4.0.1 +more-itertools==8.2.0 +moto==1.3.14 +packaging==20.3 +pluggy==0.13.1 +py==1.8.1 +pyasn1==0.4.8 +pycparser==2.20 +pyparsing==2.4.6 +pyrsistent==0.15.7 +pytest==5.3.5 +pytest-cov==2.8.1 +pytest-mock==2.0.0 +python-dateutil==2.8.1 +python-jose==3.1.0 +pytz==2019.3 +PyYAML==5.3 +requests==2.23.0 +responses==0.10.12 +rsa==4.0 +s3transfer==0.3.3 +six==1.14.0 +sshpubkeys==3.1.0 +urllib3==1.25.8 +wcwidth==0.1.8 +websocket-client==0.57.0 +Werkzeug==1.0.0 +wrapt==1.12.0 +xmltodict==0.12.0 +zipp==3.1.0 diff --git a/test_vector_handlers/README.rst b/test_vector_handlers/README.rst index a2d188dd6..b1d10c2c5 100644 --- a/test_vector_handlers/README.rst +++ b/test_vector_handlers/README.rst @@ -12,7 +12,7 @@ Getting Started Required Prerequisites ====================== -* Python 2.7 or 3.4+ +* Python 2.7 or 3.5+ * aws-encryption-sdk Use diff --git a/test_vector_handlers/setup.py b/test_vector_handlers/setup.py index bd16c76c8..c7ad742ad 100644 --- a/test_vector_handlers/setup.py +++ b/test_vector_handlers/setup.py @@ -48,10 +48,10 @@ def get_requirements(): "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Security", "Topic :: Security :: Cryptography", diff --git a/test_vector_handlers/tox.ini b/test_vector_handlers/tox.ini index fc2c8d5a7..d62bc38ea 100644 --- a/test_vector_handlers/tox.ini +++ b/test_vector_handlers/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - py{27,34,35,36,37}-awses_{1.3.3,1.3.max,latest}, + py{27,35,36,37,38}-awses_{1.3.3,1.3.max,latest}, # 1.2.0 and 1.2.max are being difficult because of attrs bandit, doc8, readme, docs, {flake8,pylint}{,-tests}, diff --git a/tox.ini b/tox.ini index 08ec65251..ccc5dc4fa 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - py{27,34,35,36,37}-{local,integ,accept,examples}, nocmk, + py{27,35,36,37,38}-{local,integ,accept,examples}, nocmk, bandit, doc8, readme, docs, {flake8,pylint}{,-tests,-examples}, isort-check, black-check, @@ -40,8 +40,9 @@ commands = pytest --basetemp={envtmpdir} -l --cov aws_encryption_sdk {posargs} [testenv] passenv = - # Identifies AWS KMS key id to use in integration tests + # Identifies AWS KMS key ids to use in integration tests AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID \ + AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID_2 \ # Pass through AWS credentials AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN \ # Pass through AWS profile name (useful for local testing) @@ -139,6 +140,10 @@ commands = [testenv:flake8-examples] basepython = {[testenv:flake8]basepython} +# This does not actually ignore errors, +# it just runs all commands regardless of whether any fail. +# If any fail, the final result will still fail. +ignore_errors = true deps = {[testenv:flake8]deps} commands = flake8 examples/src/ @@ -163,6 +168,10 @@ commands = [testenv:pylint-examples] basepython = {[testenv:pylint]basepython} +# This does not actually ignore errors, +# it just runs all commands regardless of whether any fail. +# If any fail, the final result will still fail. +ignore_errors = true deps = {[testenv:pylint]deps} commands = pylint --rcfile=examples/src/pylintrc examples/src/ @@ -231,9 +240,11 @@ commands = {[testenv:isort]commands} -c [testenv:autoformat] basepython = python3 deps = + {[testenv:isort-seed]deps} {[testenv:blacken]deps} {[testenv:isort]deps} commands = + {[testenv:isort-seed]commands} {[testenv:blacken]commands} {[testenv:isort]commands}