From 8354d770d0d3845e995ad96f22ab8fda84ec59d2 Mon Sep 17 00:00:00 2001 From: Valerie Lambert Date: Tue, 20 Apr 2021 15:49:50 -0700 Subject: [PATCH] feat: AWS KMS multi-Region Key support Added new two new keyrings AwsKmsMrkAwareSymmetricKeyringNode and AwsKmsMrkAwareSymmetricDiscoveryKeyringNode that support AWS KMS multi-Region Keys. Added the helpers buildAwsKmsMrkAwareStrictMultiKeyringNode and buildAwsKmsMrkAwareDiscoveryMultiKeyringNode that compose the above keyring together to handle multiple CMKs. See https://docs.aws.amazon.com/kms/latest/developerguide/multi-region-keys-overview.html for more details about AWS KMS multi-Region Keys. See https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/configure.html#config-mrks for more details about how the AWS Encryption SDK interoperates with AWS KMS multi-Region keys. --- .gitignore | 1 + .gitmodules | 4 +- aws-encryption-sdk-specification | 1 + aws-encryption-sdk-test-vectors | 2 +- buildspec.yml | 22 +- codebuild/browser.yml | 16 + codebuild/compliance.yml | 16 + codebuild/nodejs10.yml | 24 +- codebuild/nodejs12.yml | 24 +- codebuild/nodejs14.yml | 16 + codebuild/test_vectors/browser.yml | 18 + codebuild/test_vectors/nodejs10.yml | 18 + codebuild/test_vectors/nodejs12.yml | 18 + codebuild/test_vectors/nodejs14.yml | 18 + compliance_exceptions/master-key-exception.ts | 183 ++++ .../master-key-provider-exception.ts | 231 +++++ .../src/kms_multi_region_discovery.ts | 80 ++ .../src/kms_multi_region_simple.ts | 110 +++ modules/example-browser/test/index.test.ts | 14 + .../src/kms_multi_region_discovery.ts | 68 ++ .../src/kms_multi_region_simple.ts | 91 ++ modules/example-node/test/index.test.ts | 20 +- modules/integration-browser/karma.conf.js | 25 +- .../src/build_decrypt_fixtures.ts | 5 +- .../decrypt_materials_manager_web_crypto.ts | 62 +- .../src/testDecryptFixture.ts | 23 +- .../src/decrypt_materials_manager_node.ts | 54 +- modules/integration-vectors/package.json | 2 +- .../src/build_get_keyring.ts | 62 ++ .../src/get_decrypt_test_iterator.ts | 3 + modules/integration-vectors/src/index.ts | 1 + modules/integration-vectors/src/types.ts | 28 +- .../test/get_decrypt_test_iterator.test.ts | 46 + modules/kms-keyring-browser/src/index.ts | 4 + .../src/kms_keyring_browser.ts | 10 +- .../src/kms_mrk_discovery_keyring_browser.ts | 54 ++ ...kms_mrk_discovery_multi_keyring_browser.ts | 20 + .../src/kms_mrk_keyring_browser.ts | 56 ++ .../kms_mrk_strict_multi_keyring_browser.ts | 16 + .../kms_mrk_discovery_keyring_browser.test.ts | 105 +++ .../test/kms_mrk_keyring_browser.test.ts | 114 +++ modules/kms-keyring-node/src/index.ts | 4 + .../kms-keyring-node/src/kms_keyring_node.ts | 8 +- .../src/kms_mrk_discovery_keyring_node.ts | 20 + .../kms_mrk_discovery_multi_keyring_node.ts | 29 + .../src/kms_mrk_keyring_node.ts | 20 + .../src/kms_mrk_strict_multi_keyring_node.ts | 24 + .../kms_mrk_discovery_keyring_node.test.ts | 101 +++ .../test/kms_mrk_keyring_node.test.ts | 121 +++ modules/kms-keyring/src/arn_parsing.ts | 274 ++++++ .../kms-keyring/src/aws_kms_mrk_are_unique.ts | 44 + modules/kms-keyring/src/helpers.ts | 27 +- modules/kms-keyring/src/index.ts | 4 + modules/kms-keyring/src/kms_keyring.ts | 38 +- .../src/kms_mrk_discovery_keyring.ts | 326 +++++++ .../src/kms_mrk_discovery_multi_keyring.ts | 105 +++ modules/kms-keyring/src/kms_mrk_keyring.ts | 393 +++++++++ .../src/kms_mrk_strict_multi_keyring.ts | 144 ++++ .../src/region_from_kms_key_arn.ts | 14 +- modules/kms-keyring/test/arn_parsing.test.ts | 471 ++++++++++ .../test/aws_kms_mrk_are_unique.test.ts | 154 ++++ .../test/kms_keyring.constructor.test.ts | 36 +- .../test/kms_keyring.edk-order.test.ts | 33 +- .../test/kms_keyring.ondecrypt.test.ts | 59 +- .../test/kms_keyring.onencrypt.test.ts | 17 +- ..._mrk_discovery_keyring.constructor.test.ts | 104 +++ ...ms_mrk_discovery_keyring.edk-order.test.ts | 158 ++++ ...ms_mrk_discovery_keyring.ondecrypt.test.ts | 807 ++++++++++++++++++ ...ms_mrk_discovery_keyring.onencrypt.test.ts | 47 + .../kms_mrk_discovery_multi_keyring.test.ts | 157 ++++ .../test/kms_mrk_keyring.constructor.test.ts | 140 +++ .../test/kms_mrk_keyring.edk-order.test.ts | 154 ++++ .../test/kms_mrk_keyring.ondecrypt.test.ts | 653 ++++++++++++++ .../test/kms_mrk_keyring.onencrypt.test.ts | 438 ++++++++++ .../test/kms_mrk_strict_multi_keyring.test.ts | 246 ++++++ .../material-management-browser/src/index.ts | 1 + modules/material-management-node/src/index.ts | 1 + modules/material-management/src/index.ts | 8 +- .../material-management/src/multi_keyring.ts | 3 +- modules/material-management/src/types.ts | 2 + package.json | 13 +- util/test_conditions | 95 --- 82 files changed, 6882 insertions(+), 296 deletions(-) create mode 160000 aws-encryption-sdk-specification create mode 100644 codebuild/browser.yml create mode 100644 codebuild/compliance.yml create mode 100644 codebuild/nodejs14.yml create mode 100644 codebuild/test_vectors/browser.yml create mode 100644 codebuild/test_vectors/nodejs10.yml create mode 100644 codebuild/test_vectors/nodejs12.yml create mode 100644 codebuild/test_vectors/nodejs14.yml create mode 100644 compliance_exceptions/master-key-exception.ts create mode 100644 compliance_exceptions/master-key-provider-exception.ts create mode 100644 modules/example-browser/src/kms_multi_region_discovery.ts create mode 100644 modules/example-browser/src/kms_multi_region_simple.ts create mode 100644 modules/example-node/src/kms_multi_region_discovery.ts create mode 100644 modules/example-node/src/kms_multi_region_simple.ts create mode 100644 modules/integration-vectors/src/build_get_keyring.ts create mode 100644 modules/kms-keyring-browser/src/kms_mrk_discovery_keyring_browser.ts create mode 100644 modules/kms-keyring-browser/src/kms_mrk_discovery_multi_keyring_browser.ts create mode 100644 modules/kms-keyring-browser/src/kms_mrk_keyring_browser.ts create mode 100644 modules/kms-keyring-browser/src/kms_mrk_strict_multi_keyring_browser.ts create mode 100644 modules/kms-keyring-browser/test/kms_mrk_discovery_keyring_browser.test.ts create mode 100644 modules/kms-keyring-browser/test/kms_mrk_keyring_browser.test.ts create mode 100644 modules/kms-keyring-node/src/kms_mrk_discovery_keyring_node.ts create mode 100644 modules/kms-keyring-node/src/kms_mrk_discovery_multi_keyring_node.ts create mode 100644 modules/kms-keyring-node/src/kms_mrk_keyring_node.ts create mode 100644 modules/kms-keyring-node/src/kms_mrk_strict_multi_keyring_node.ts create mode 100644 modules/kms-keyring-node/test/kms_mrk_discovery_keyring_node.test.ts create mode 100644 modules/kms-keyring-node/test/kms_mrk_keyring_node.test.ts create mode 100644 modules/kms-keyring/src/arn_parsing.ts create mode 100644 modules/kms-keyring/src/aws_kms_mrk_are_unique.ts create mode 100644 modules/kms-keyring/src/kms_mrk_discovery_keyring.ts create mode 100644 modules/kms-keyring/src/kms_mrk_discovery_multi_keyring.ts create mode 100644 modules/kms-keyring/src/kms_mrk_keyring.ts create mode 100644 modules/kms-keyring/src/kms_mrk_strict_multi_keyring.ts create mode 100644 modules/kms-keyring/test/arn_parsing.test.ts create mode 100644 modules/kms-keyring/test/aws_kms_mrk_are_unique.test.ts create mode 100644 modules/kms-keyring/test/kms_mrk_discovery_keyring.constructor.test.ts create mode 100644 modules/kms-keyring/test/kms_mrk_discovery_keyring.edk-order.test.ts create mode 100644 modules/kms-keyring/test/kms_mrk_discovery_keyring.ondecrypt.test.ts create mode 100644 modules/kms-keyring/test/kms_mrk_discovery_keyring.onencrypt.test.ts create mode 100644 modules/kms-keyring/test/kms_mrk_discovery_multi_keyring.test.ts create mode 100644 modules/kms-keyring/test/kms_mrk_keyring.constructor.test.ts create mode 100644 modules/kms-keyring/test/kms_mrk_keyring.edk-order.test.ts create mode 100644 modules/kms-keyring/test/kms_mrk_keyring.ondecrypt.test.ts create mode 100644 modules/kms-keyring/test/kms_mrk_keyring.onencrypt.test.ts create mode 100644 modules/kms-keyring/test/kms_mrk_strict_multi_keyring.test.ts delete mode 100755 util/test_conditions diff --git a/.gitignore b/.gitignore index 32b6e6798..21584cf87 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,7 @@ package.json.decrypt # local npx cache /verdaccio/.npx +/specification_compliance_report.html # These version files are build by genversion # they track the package.json version diff --git a/.gitmodules b/.gitmodules index 156471e62..8163db162 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,6 @@ [submodule "aws-encryption-sdk-test-vectors"] path = aws-encryption-sdk-test-vectors url = https://github.com/awslabs/private-aws-encryption-sdk-test-vectors-staging.git - branch = known-invalid-test-vectors +[submodule "aws-encryption-sdk-specification"] + path = aws-encryption-sdk-specification + url = https://github.com/awslabs/private-aws-encryption-sdk-specification-staging.git diff --git a/aws-encryption-sdk-specification b/aws-encryption-sdk-specification new file mode 160000 index 000000000..ef3420d0f --- /dev/null +++ b/aws-encryption-sdk-specification @@ -0,0 +1 @@ +Subproject commit ef3420d0fa8740c4a98f2e9e976d75be185473e4 diff --git a/aws-encryption-sdk-test-vectors b/aws-encryption-sdk-test-vectors index 3fdc7c682..5cb6870e3 160000 --- a/aws-encryption-sdk-test-vectors +++ b/aws-encryption-sdk-test-vectors @@ -1 +1 @@ -Subproject commit 3fdc7c682184a3c7214686550fafb490c14930c4 +Subproject commit 5cb6870e3d9e0d7117220c0f0033451818f25e85 diff --git a/buildspec.yml b/buildspec.yml index 9026793bc..7a0a84fee 100644 --- a/buildspec.yml +++ b/buildspec.yml @@ -3,7 +3,25 @@ version: 0.2 batch: fast-fail: false build-list: - - identifier: nodejs10 + - identifier: testNodejs10 buildspec: codebuild/nodejs10.yml - - identifier: nodejs12 + - identifier: testNodejs12 buildspec: codebuild/nodejs12.yml + - identifier: testNodejs14 + buildspec: codebuild/nodejs14.yml + env: + image: aws/codebuild/standard:5.0 + - identifier: testBrowser + buildspec: codebuild/browser.yml + - identifier: compliance + buildspec: codebuild/compliance.yml + - identifier: testVectorsNodejs10 + buildspec: codebuild/test_vectors/nodejs10.yml + - identifier: testVectorsNodejs12 + buildspec: codebuild/test_vectors/nodejs12.yml + - identifier: testVectorsNodejs14 + buildspec: codebuild/test_vectors/nodejs14.yml + env: + image: aws/codebuild/standard:5.0 + - identifier: testVectorsBrowser + buildspec: codebuild/test_vectors/browser.yml diff --git a/codebuild/browser.yml b/codebuild/browser.yml new file mode 100644 index 000000000..e1004b776 --- /dev/null +++ b/codebuild/browser.yml @@ -0,0 +1,16 @@ +version: 0.2 + +env: + variables: + NODE_OPTIONS: "--max-old-space-size=4096" + +phases: + install: + runtime-versions: + nodejs: latest + commands: + - npm ci --unsafe-perm + - npm run build + build: + commands: + - npm run coverage-browser diff --git a/codebuild/compliance.yml b/codebuild/compliance.yml new file mode 100644 index 000000000..7ad8ee335 --- /dev/null +++ b/codebuild/compliance.yml @@ -0,0 +1,16 @@ +version: 0.2 + +env: + variables: + NODE_OPTIONS: "--max-old-space-size=4096" + +phases: + install: + runtime-versions: + nodejs: latest + commands: + - npm ci --unsafe-perm + build: + commands: + - npm run lint + - npm run test_conditions diff --git a/codebuild/nodejs10.yml b/codebuild/nodejs10.yml index 1e42729c4..06f218b40 100644 --- a/codebuild/nodejs10.yml +++ b/codebuild/nodejs10.yml @@ -1,18 +1,16 @@ version: 0.2 env: - variables: - NODE_OPTIONS: "--max-old-space-size=4096" + variables: + NODE_OPTIONS: "--max-old-space-size=4096" phases: - install: - runtime-versions: - nodejs: 10 - commands: - - npm ci --unsafe-perm - - npm run build - build: - commands: - - npm test - - npm run test_conditions - - npm run verdaccio + install: + runtime-versions: + nodejs: 10 + commands: + - npm ci --unsafe-perm + - npm run build + build: + commands: + - npm run coverage-node diff --git a/codebuild/nodejs12.yml b/codebuild/nodejs12.yml index 6c87b53fa..23565c46e 100644 --- a/codebuild/nodejs12.yml +++ b/codebuild/nodejs12.yml @@ -1,18 +1,16 @@ version: 0.2 env: - variables: - NODE_OPTIONS: "--max-old-space-size=4096" + variables: + NODE_OPTIONS: "--max-old-space-size=4096" phases: - install: - runtime-versions: - nodejs: 12 - commands: - - npm ci --unsafe-perm - - npm run build - build: - commands: - - npm test - - npm run test_conditions - - npm run verdaccio + install: + runtime-versions: + nodejs: 12 + commands: + - npm ci --unsafe-perm + - npm run build + build: + commands: + - npm run coverage-node diff --git a/codebuild/nodejs14.yml b/codebuild/nodejs14.yml new file mode 100644 index 000000000..ffa3816b4 --- /dev/null +++ b/codebuild/nodejs14.yml @@ -0,0 +1,16 @@ +version: 0.2 + +env: + variables: + NODE_OPTIONS: "--max-old-space-size=4096" + +phases: + install: + runtime-versions: + nodejs: 14 + commands: + - npm ci --unsafe-perm + - npm run build + build: + commands: + - npm run coverage-node diff --git a/codebuild/test_vectors/browser.yml b/codebuild/test_vectors/browser.yml new file mode 100644 index 000000000..43fc2fafa --- /dev/null +++ b/codebuild/test_vectors/browser.yml @@ -0,0 +1,18 @@ +version: 0.2 + +env: + variables: + NODE_OPTIONS: "--max-old-space-size=4096" + +phases: + install: + runtime-versions: + nodejs: latest + commands: + - npm ci --unsafe-perm + - npm run build + build: + commands: + - npm run verdaccio-publish + - npm run verdaccio-publish-browser-decrypt + - npm run verdaccio-publish-browser-encrypt diff --git a/codebuild/test_vectors/nodejs10.yml b/codebuild/test_vectors/nodejs10.yml new file mode 100644 index 000000000..3242d2eb2 --- /dev/null +++ b/codebuild/test_vectors/nodejs10.yml @@ -0,0 +1,18 @@ +version: 0.2 + +env: + variables: + NODE_OPTIONS: "--max-old-space-size=4096" + +phases: + install: + runtime-versions: + nodejs: 10 + commands: + - npm ci --unsafe-perm + - npm run build + build: + commands: + - npm run verdaccio-publish + - npm run verdaccio-publish-node-decrypt + - npm run verdaccio-publish-node-encrypt diff --git a/codebuild/test_vectors/nodejs12.yml b/codebuild/test_vectors/nodejs12.yml new file mode 100644 index 000000000..8ad393506 --- /dev/null +++ b/codebuild/test_vectors/nodejs12.yml @@ -0,0 +1,18 @@ +version: 0.2 + +env: + variables: + NODE_OPTIONS: "--max-old-space-size=4096" + +phases: + install: + runtime-versions: + nodejs: 12 + commands: + - npm ci --unsafe-perm + - npm run build + build: + commands: + - npm run verdaccio-publish + - npm run verdaccio-publish-node-decrypt + - npm run verdaccio-publish-node-encrypt diff --git a/codebuild/test_vectors/nodejs14.yml b/codebuild/test_vectors/nodejs14.yml new file mode 100644 index 000000000..58ec78909 --- /dev/null +++ b/codebuild/test_vectors/nodejs14.yml @@ -0,0 +1,18 @@ +version: 0.2 + +env: + variables: + NODE_OPTIONS: "--max-old-space-size=4096" + +phases: + install: + runtime-versions: + nodejs: 14 + commands: + - npm ci --unsafe-perm + - npm run build + build: + commands: + - npm run verdaccio-publish + - npm run verdaccio-publish-node-decrypt + - npm run verdaccio-publish-node-encrypt diff --git a/compliance_exceptions/master-key-exception.ts b/compliance_exceptions/master-key-exception.ts new file mode 100644 index 000000000..faf5e0f41 --- /dev/null +++ b/compliance_exceptions/master-key-exception.ts @@ -0,0 +1,183 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// The AWS Encryption SDK - JS does not implement +// any of the legacy Master Key interface. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.5 +//= type=exception +//# MUST implement the Master Key Interface (../master-key- +//# interface.md#interface) + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.6 +//= type=exception +//# On initialization, the caller MUST provide: + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.6 +//= type=exception +//# The AWS KMS key identifier MUST NOT be null or empty. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.6 +//= type=exception +//# The AWS KMS +//# key identifier MUST be a valid identifier (aws-kms-key-arn.md#a- +//# valid-aws-kms-identifier). + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.6 +//= type=exception +//# The AWS KMS +//# SDK client MUST not be null. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.6 +//= type=exception +//# The master key MUST be able to be +//# configured with an optional list of Grant Tokens. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.6 +//= type=exception +//# This configuration +//# SHOULD be on initialization and SHOULD be immutable. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.7 +//= type=exception +//# MUST be unchanged from the Master Key interface. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.8 +//= type=exception +//# MUST be unchanged from the Master Key interface. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 +//= type=exception +//# The inputs MUST be the same as the Master Key Decrypt Data Key +//# (../master-key-interface.md#decrypt-data-key) interface. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 +//= type=exception +//# The set of encrypted data keys MUST first be filtered to match this +//# master key's configuration. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 +//= type=exception +//# To match the encrypted data key's +//# provider ID MUST exactly match the value "aws-kms" and the the +//# function AWS KMS MRK Match for Decrypt (aws-kms-mrk-match-for- +//# decrypt.md#implementation) called with the configured AWS KMS key +//# identifier and the encrypted data key's provider info MUST return +//# "true". + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 +//= type=exception +//# Additionally each provider info MUST be a valid AWS KMS ARN +//# (aws-kms-key-arn.md#a-valid-aws-kms-arn) with a resource type of +//# "key". + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 +//= type=exception +//# For each encrypted data key in the filtered set, one at a time, the +//# master key MUST attempt to decrypt the data key. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 +//= type=exception +//# If this attempt +//# results in an error, then these errors MUST be collected. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 +//= type=exception +//# To decrypt the encrypted data key this master key MUST use the +//# configured AWS KMS client to make an AWS KMS Decrypt +//# (https://docs.aws.amazon.com/kms/latest/APIReference/ +//# API_Decrypt.html) request constructed as follows: + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 +//= type=exception +//# If the call succeeds then the response's "KeyId" MUST be equal to the +//# configured AWS KMS key identifier otherwise the function MUST collect +//# an error. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 +//= type=exception +//# The response's "Plaintext"'s length MUST equal the length +//# required by the requested algorithm suite otherwise the function MUST +//# collect an error. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 +//= type=exception +//# If the AWS KMS response satisfies the requirements then it MUST be +//# use and this function MUST return and not attempt to decrypt any more +//# encrypted data keys. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 +//= type=exception +//# If all the input encrypted data keys have been processed then this +//# function MUST yield an error that includes all the collected errors. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 +//= type=exception +//# The output MUST be the same as the Master Key Decrypt Data Key +//# (../master-key-interface.md#decrypt-data-key) interface. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.10 +//= type=exception +//# The inputs MUST be the same as the Master Key Generate Data Key +//# (../master-key-interface.md#generate-data-key) interface. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.10 +//= type=exception +//# This +//# master key MUST use the configured AWS KMS client to make an AWS KMS +//# GenerateDatakey (https://docs.aws.amazon.com/kms/latest/APIReference/ +//# API_GenerateDataKey.html) request constructed as follows: + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.10 +//= type=exception +//# If the call succeeds the AWS KMS Generate Data Key response's +//# "Plaintext" MUST match the key derivation input length specified by +//# the algorithm suite included in the input. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.10 +//= type=exception +//# The response's "KeyId" MUST be valid. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.10 +//= type=exception +//# The response's "Plaintext" MUST be the plaintext in the output. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.10 +//= type=exception +//# The +//# response's cipher text blob MUST be used as the returned as the +//# ciphertext for the encrypted data key in the output. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.10 +//= type=exception +//# The output MUST be the same as the Master Key Generate Data Key +//# (../master-key-interface.md#generate-data-key) interface. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.11 +//= type=exception +//# The inputs MUST be the same as the Master Key Encrypt Data Key +//# (../master-key-interface.md#encrypt-data-key) interface. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.11 +//= type=exception +//# The master +//# key MUST use the configured AWS KMS client to make an AWS KMS Encrypt +//# (https://docs.aws.amazon.com/kms/latest/APIReference/ +//# API_Encrypt.html) request constructed as follows: + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.11 +//= type=exception +//# The AWS KMS Encrypt response MUST contain a valid "KeyId". + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.11 +//= type=exception +//# The +//# response's cipher text blob MUST be used as the "ciphertext" for the +//# encrypted data key. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.11 +//= type=exception +//# The output MUST be the same as the Master Key Encrypt Data Key +//# (../master-key-interface.md#encrypt-data-key) interface. + +// There appears to be something about the end of the file? diff --git a/compliance_exceptions/master-key-provider-exception.ts b/compliance_exceptions/master-key-provider-exception.ts new file mode 100644 index 000000000..5850c7b8c --- /dev/null +++ b/compliance_exceptions/master-key-provider-exception.ts @@ -0,0 +1,231 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// The AWS Encryption SDK - JS does not implement +// any of the legacy Master Key Provider interface. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.5 +//= type=exception +//# MUST implement the Master Key Provider Interface (../master-key- +//# provider-interface.md#interface) + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.6 +//= type=exception +//# On initialization the caller MUST provide: + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.6 +//= type=exception +//# The key id list MUST NOT be empty or null in strict mode. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.6 +//= type=exception +//# The key id +//# list MUST NOT contain any null or empty string values. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.6 +//= type=exception +//# All AWS KMS +//# key identifiers are be passed to Assert AWS KMS MRK are unique (aws- +//# kms-mrk-are-unique.md#Implementation) and the function MUST return +//# success. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.6 +//= type=exception +//# A +//# discovery filter MUST NOT be configured in strict mode. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.6 +//= type=exception +//# A default +//# MRK Region MUST NOT be configured in strict mode. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.6 +//= type=exception +//# In discovery mode +//# if a default MRK Region is not configured the AWS SDK Default Region +//# MUST be used. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.6 +//= type=exception +//# If an AWS SDK Default Region can not be obtained +//# initialization MUST fail. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.6 +//= type=exception +//# The key id list MUST be empty in discovery mode. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.6 +//= type=exception +//# The regional client +//# supplier MUST be defined in discovery mode. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 +//= type=exception +//# The input MUST be the same as the Master Key Provider Get Master Key +//# (../master-key-provider-interface.md#get-master-key) interface. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 +//= type=exception +//# The function MUST only provide master keys if the input provider id +//# equals "aws-kms". + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 +//= type=exception +//# In strict mode, the requested AWS KMS key ARN MUST +//# match a member of the configured key ids by using AWS KMS MRK Match +//# for Decrypt (aws-kms-mrk-match-for-decrypt.md#implementation) +//# otherwise this function MUST error. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 +//= type=exception +//# In discovery mode, the requested +//# AWS KMS key identifier MUST be a well formed AWS KMS ARN. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 +//= type=exception +//# In discovery mode, the requested +//# AWS KMS key identifier MUST be a well formed AWS KMS ARN. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 +//= type=exception +//# In +//# discovery mode if a discovery filter is configured the requested AWS +//# KMS key ARN's "partition" MUST match the discovery filter's +//# "partition" and the AWS KMS key ARN's "account" MUST exist in the +//# discovery filter's account id set. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 +//= type=exception +//# If the requested AWS KMS key identifier is not a well formed ARN +//# the AWS Region MUST be the configured default region this SHOULD be +//# obtained from the AWS SDK. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 +//= type=exception +//# Otherwise if the requested AWS KMS key +//# identifier is identified as a multi-Region key (aws-kms-key- +//# arn.md#identifying-an-aws-kms-multi-region-key), then AWS Region MUST +//# be the region from the AWS KMS key ARN stored in the provider info +//# from the encrypted data key. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 +//= type=exception +//# Otherwise if the mode is discovery then +//# the AWS Region MUST be the discovery MRK region. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 +//= type=exception +//# Finally if the +//# provider info is identified as a multi-Region key (aws-kms-key- +//# arn.md#identifying-an-aws-kms-multi-region-key) the AWS Region MUST +//# be the region from the AWS KMS key in the configured key ids matched +//# to the requested AWS KMS key by using AWS KMS MRK Match for Decrypt +//# (aws-kms-mrk-match-for-decrypt.md#implementation). + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 +//= type=exception +//# An AWS KMS client +//# MUST be obtained by calling the regional client supplier with this +//# AWS Region. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 +//= type=exception +//# In strict mode a AWS KMS MRK Aware Master Key (aws-kms-mrk-aware- +//# master-key.md) MUST be returned configured with + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 +//= type=exception +//# In discovery mode a AWS KMS MRK Aware Master Key (aws-kms-mrk-aware- +//# master-key.md) MUST be returned configured with + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 +//= type=exception +//# The output MUST be the same as the Master Key Provider Get Master Key +//# (../master-key-provider-interface.md#get-master-key) interface. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.8 +//= type=exception +//# The input MUST be the same as the Master Key Provider Get Master Keys +//# For Encryption (../master-key-provider-interface.md#get-master-keys- +//# for-encryption) interface. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.8 +//= type=exception +//# If the configured mode is discovery the function MUST return an empty +//# list. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.8 +//= type=exception +//# If the configured mode is strict this function MUST return a +//# list of master keys obtained by calling Get Master Key (aws-kms-mrk- +//# aware-master-key-provider.md#get-master-key) for each AWS KMS key +//# identifier in the configured key ids + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.8 +//= type=exception +//# The output MUST be the same as the Master Key Provider Get Master +//# Keys For Encryption (../master-key-provider-interface.md#get-master- +//# keys-for-encryption) interface. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.9 +//= type=exception +//# The input MUST be the same as the Master Key Provider Decrypt Data +//# Key (../master-key-provider-interface.md#decrypt-data-key) interface. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.9 +//= type=exception +//# The set of encrypted data keys MUST first be filtered to match this +//# master key's configuration. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.9 +//= type=exception +//# To match the encrypted data key's +//# provider ID MUST exactly match the value "aws-kms". + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.9 +//= type=exception +//# Additionally +//# each provider info MUST be a valid AWS KMS ARN (aws-kms-key-arn.md#a- +//# valid-aws-kms-arn) with a resource type of "key". + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.9 +//= type=exception +//# For each encrypted data key in the filtered set, one at a time, the +//# master key provider MUST call Get Master Key (aws-kms-mrk-aware- +//# master-key-provider.md#get-master-key) with the encrypted data key's +//# provider info as the AWS KMS key ARN. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.9 +//= type=exception +//# It MUST call Decrypt Data Key +//# (aws-kms-mrk-aware-master-key.md#decrypt-data-key) on this master key +//# with the input algorithm, this single encrypted data key, and the +//# input encryption context. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.9 +//= type=exception +//# If this attempt results in an error, then +//# these errors MUST be collected. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.9 +//= type=exception +//# If the decrypt data key call is +//# successful, then this function MUST return this result and not +//# attempt to decrypt any more encrypted data keys. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.9 +//= type=exception +//# If all the input encrypted data keys have been processed then this +//# function MUST yield an error that includes all the collected errors. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.9 +//= type=exception +//# The output MUST be the same as the Master Key Provider Decrypt Data +//# Key (../master-key-provider-interface.md#decrypt-data-key) interface. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.9 +//= type=exception +//# The output MUST be the same as the Master Key Provider Decrypt Data +//# Key (../master-key-provider-interface.md#decrypt-data-key) interface. + +// There appears to be something about the end of the file? diff --git a/modules/example-browser/src/kms_multi_region_discovery.ts b/modules/example-browser/src/kms_multi_region_discovery.ts new file mode 100644 index 000000000..3088a6940 --- /dev/null +++ b/modules/example-browser/src/kms_multi_region_discovery.ts @@ -0,0 +1,80 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + AwsKmsMrkAwareSymmetricDiscoveryKeyringBrowser, + buildClient, + CommitmentPolicy, + KMS, +} from '@aws-crypto/client-browser' + +/* This builds the client with the REQUIRE_ENCRYPT_REQUIRE_DECRYPT commitment policy, + * which enforces that this client only encrypts using committing algorithm suites + * and enforces that this client + * will only decrypt encrypted messages + * that were created with a committing algorithm suite. + * This is the default commitment policy + * if you build the client with `buildClient()`. + */ +const { decrypt } = buildClient( + CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT +) + +/* This is injected by webpack. + * The webpack.DefinePlugin will replace the values when bundling. + * The credential values are pulled from @aws-sdk/credential-provider-node + * Use any method you like to get credentials into the browser. + * See kms.webpack.config + */ +declare const credentials: { + accessKeyId: string + secretAccessKey: string + sessionToken: string +} + +export async function kmsMultiRegionDiscoveryTest(ciphertext: Uint8Array) { + /* Create an AWS KMS Client. + * This client will be used for all multi-Region keys. + */ + const client = new KMS({ region: 'us-west-2', credentials }) + /* Create discovery filter for decrypting. + * This filter restricts what AWS KMS CMKs + * the AWS KMS multi region optimized master key provider can use + * to those in a particular AWS partition and account. + * You can create a similar filter with one partition and multiple AWS accounts. + * This example only configures the filter with one account, + * but more may be specified as long as they exist within the same partition. + * This filter is not required for Discovery mode, but is a best practice. + */ + + const discoveryFilter = { partition: 'aws', accountIDs: ['658956600833'] } + + /* Instantiate an AwsKmsMrkAwareSymmetricDiscoveryKeyringNode with the client and filter. */ + const keyring = new AwsKmsMrkAwareSymmetricDiscoveryKeyringBrowser({ + client, + discoveryFilter, + }) + + /* Decrypt the data. */ + const { messageHeader, plaintext } = await decrypt(keyring, ciphertext) + + /* Verify the encryption context. + * If you use an algorithm suite with signing, + * the Encryption SDK adds a name-value pair to the encryption context that contains the public key. + * Because the encryption context might contain additional key-value pairs, + * do not add a test that requires that all key-value pairs match. + * Instead, verify that the key-value pairs you expect match. + */ + const context = { + stage: 'demo', + purpose: 'simple demonstration app', + origin: 'us-west-2', + } + Object.entries(context).forEach(([key, value]) => { + if (messageHeader.encryptionContext[key] !== value) + throw new Error('Encryption Context does not match expected values') + }) + + /* Return the values so the code can be tested. */ + return { messageHeader, plaintext } +} diff --git a/modules/example-browser/src/kms_multi_region_simple.ts b/modules/example-browser/src/kms_multi_region_simple.ts new file mode 100644 index 000000000..ebabc1677 --- /dev/null +++ b/modules/example-browser/src/kms_multi_region_simple.ts @@ -0,0 +1,110 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + buildAwsKmsMrkAwareStrictMultiKeyringBrowser, + buildClient, + CommitmentPolicy, + KMS, +} from '@aws-crypto/client-browser' +/* This builds the client with the REQUIRE_ENCRYPT_REQUIRE_DECRYPT commitment policy, + * which enforces that this client only encrypts using committing algorithm suites + * and enforces that this client + * will only decrypt encrypted messages + * that were created with a committing algorithm suite. + * This is the default commitment policy + * if you build the client with `buildClient()`. + */ +const { encrypt, decrypt } = buildClient( + CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT +) + +/* This is injected by webpack. + * The webpack.DefinePlugin will replace the values when bundling. + * The credential values are pulled from @aws-sdk/credential-provider-node + * Use any method you like to get credentials into the browser. + * See kms.webpack.config + */ +declare const credentials: { + accessKeyId: string + secretAccessKey: string + sessionToken: string +} +/* The credentials can be injected here, + * because browser do not have a standard credential discover process the way Node.js does. + */ +const clientProvider = (region: string) => new KMS({ region, credentials }) + +export async function kmsMultiRegionSimpleTest() { + /* A KMS CMK is required to generate the data key. + * You need kms:GenerateDataKey permission on the CMK in generatorKeyId. + * In this example we are using two related multi-Region keys. + * We will encrypt with the us-east-1 multi-Region key first. + */ + const multiRegionUsEastKey = + 'arn:aws:kms:us-east-1:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7' + + /* The AWS KMS MRK Aware keyring must be configured with the related CMK. */ + const encryptKeyring = buildAwsKmsMrkAwareStrictMultiKeyringBrowser({ + generatorKeyId: multiRegionUsEastKey, + clientProvider, + }) + + /* Encryption context is a *very* powerful tool for controlling and managing access. + * It is ***not*** secret! + * Encrypted data is opaque. + * You can use an encryption context to assert things about the encrypted data. + * Just because you can decrypt something does not mean it is what you expect. + * For example, if you are are only expecting data from 'us-west-2', + * the origin can identify a malicious actor. + * See: https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + */ + const context = { + stage: 'demo', + purpose: 'simple demonstration app', + origin: 'us-west-2', + } + + /* Find data to encrypt. */ + const cleartext = new Uint8Array([1, 2, 3, 4, 5]) + + /* Encrypt the data. */ + const { result } = await encrypt(encryptKeyring, cleartext, { + encryptionContext: context, + }) + + /* A KMS CMK is required to decrypt the data key. + * Access to kms:Decrypt is required for this example. + * Having encrypted with a multi-Region key in us-east-1 + * we will decrypt this message with a related multi-Region key. + */ + const multiRegionUsWestKey = + 'arn:aws:kms:us-west-2:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7' + + /* The AWS KMS MRK Aware keyring must be configured with the related CMK. */ + const decryptKeyring = buildAwsKmsMrkAwareStrictMultiKeyringBrowser({ + generatorKeyId: multiRegionUsWestKey, + clientProvider, + }) + + /* Decrypt the data. */ + const { plaintext, messageHeader } = await decrypt(decryptKeyring, result) + + /* Grab the encryption context so you can verify it. */ + const { encryptionContext } = messageHeader + + /* Verify the encryption context. + * If you use an algorithm suite with signing, + * the Encryption SDK adds a name-value pair to the encryption context that contains the public key. + * Because the encryption context might contain additional key-value pairs, + * do not add a test that requires that all key-value pairs match. + * Instead, verify that the key-value pairs you expect match. + */ + Object.entries(context).forEach(([key, value]) => { + if (encryptionContext[key] !== value) + throw new Error('Encryption Context does not match expected values') + }) + + /* Return the values so the code can be tested. */ + return { plaintext, result, cleartext, messageHeader } +} diff --git a/modules/example-browser/test/index.test.ts b/modules/example-browser/test/index.test.ts index c8d77503e..363dec5bd 100644 --- a/modules/example-browser/test/index.test.ts +++ b/modules/example-browser/test/index.test.ts @@ -19,6 +19,8 @@ import { kmsEncryptWithMaxEncryptedDataKeysTest, kmsDecryptWithMaxEncryptedDataKeysTest, } from '../src/kms_max_encrypted_data_keys' +import { kmsMultiRegionSimpleTest } from '../src/kms_multi_region_simple' +import { kmsMultiRegionDiscoveryTest } from '../src/kms_multi_region_discovery' describe('test', () => { it('testAES', async () => { @@ -101,4 +103,16 @@ describe('test', () => { 'maxEncryptedDataKeys exceeded.' ) }) + + it('kmsMultiRegionSimpleTest', async () => { + const { plaintext, cleartext } = await kmsMultiRegionSimpleTest() + expect(plaintext).to.deep.equal(cleartext) + }) + + it('kmsMultiRegionDiscoveryTest', async () => { + const { cleartext, result } = await kmsMultiRegionSimpleTest() + + const { plaintext } = await kmsMultiRegionDiscoveryTest(result) + expect(plaintext).to.deep.equal(cleartext) + }) }) diff --git a/modules/example-node/src/kms_multi_region_discovery.ts b/modules/example-node/src/kms_multi_region_discovery.ts new file mode 100644 index 000000000..454698a2c --- /dev/null +++ b/modules/example-node/src/kms_multi_region_discovery.ts @@ -0,0 +1,68 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + AwsKmsMrkAwareSymmetricDiscoveryKeyringNode, + buildClient, + CommitmentPolicy, + KMS, +} from '@aws-crypto/client-node' + +/* This builds the client with the REQUIRE_ENCRYPT_REQUIRE_DECRYPT commitment policy, + * which enforces that this client only encrypts using committing algorithm suites + * and enforces that this client + * will only decrypt encrypted messages + * that were created with a committing algorithm suite. + * This is the default commitment policy + * if you build the client with `buildClient()`. + */ +const { decrypt } = buildClient( + CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT +) + +export async function kmsMultiRegionDiscoveryTest(ciphertext: string | Buffer) { + /* Create an AWS KMS Client. + * This client will be used for all multi-Region keys. + */ + const client = new KMS({ region: 'us-west-2' }) + /* Create discovery filter for decrypting. + * This filter restricts what AWS KMS CMKs + * the AWS KMS multi region optimized master key provider can use + * to those in a particular AWS partition and account. + * You can create a similar filter with one partition and multiple AWS accounts. + * This example only configures the filter with one account, + * but more may be specified as long as they exist within the same partition. + * This filter is not required for Discovery mode, but is a best practice. + */ + + const discoveryFilter = { partition: 'aws', accountIDs: ['658956600833'] } + + /* Instantiate an AwsKmsMrkAwareSymmetricDiscoveryKeyringNode with the client and filter. */ + const keyring = new AwsKmsMrkAwareSymmetricDiscoveryKeyringNode({ + client, + discoveryFilter, + }) + + /* Decrypt the data. */ + const { messageHeader, plaintext } = await decrypt(keyring, ciphertext) + + /* Verify the encryption context. + * If you use an algorithm suite with signing, + * the Encryption SDK adds a name-value pair to the encryption context that contains the public key. + * Because the encryption context might contain additional key-value pairs, + * do not add a test that requires that all key-value pairs match. + * Instead, verify that the key-value pairs you expect match. + */ + const context = { + stage: 'demo', + purpose: 'simple demonstration app', + origin: 'us-west-2', + } + Object.entries(context).forEach(([key, value]) => { + if (messageHeader.encryptionContext[key] !== value) + throw new Error('Encryption Context does not match expected values') + }) + + /* Return the values so the code can be tested. */ + return { messageHeader, plaintext } +} diff --git a/modules/example-node/src/kms_multi_region_simple.ts b/modules/example-node/src/kms_multi_region_simple.ts new file mode 100644 index 000000000..fcb062c01 --- /dev/null +++ b/modules/example-node/src/kms_multi_region_simple.ts @@ -0,0 +1,91 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + buildAwsKmsMrkAwareStrictMultiKeyringNode, + buildClient, + CommitmentPolicy, +} from '@aws-crypto/client-node' +/* This builds the client with the REQUIRE_ENCRYPT_REQUIRE_DECRYPT commitment policy, + * which enforces that this client only encrypts using committing algorithm suites + * and enforces that this client + * will only decrypt encrypted messages + * that were created with a committing algorithm suite. + * This is the default commitment policy + * if you build the client with `buildClient()`. + */ +const { encrypt, decrypt } = buildClient( + CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT +) + +export async function kmsMultiRegionSimpleTest() { + /* A KMS CMK is required to generate the data key. + * You need kms:GenerateDataKey permission on the CMK in generatorKeyId. + * In this example we are using two related multi-Region keys. + * We will encrypt with the us-east-1 multi-Region key first. + */ + const multiRegionUsEastKey = + 'arn:aws:kms:us-east-1:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7' + + /* The AWS KMS MRK Aware keyring must be configured with the related CMK. */ + const encryptKeyring = buildAwsKmsMrkAwareStrictMultiKeyringNode({ + generatorKeyId: multiRegionUsEastKey, + }) + + /* Encryption context is a *very* powerful tool for controlling and managing access. + * It is ***not*** secret! + * Encrypted data is opaque. + * You can use an encryption context to assert things about the encrypted data. + * Just because you can decrypt something does not mean it is what you expect. + * For example, if you are are only expecting data from 'us-west-2', + * the origin can identify a malicious actor. + * See: https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + */ + const context = { + stage: 'demo', + purpose: 'simple demonstration app', + origin: 'us-west-2', + } + + /* Find data to encrypt. A simple string. */ + const cleartext = 'asdf' + + /* Encrypt the data. */ + const { result } = await encrypt(encryptKeyring, cleartext, { + encryptionContext: context, + }) + + /* A KMS CMK is required to decrypt the data key. + * Access to kms:Decrypt is required for this example. + * Having encrypted with a multi-Region key in us-east-1 + * we will decrypt this message with a related multi-Region key. + */ + const multiRegionUsWestKey = + 'arn:aws:kms:us-west-2:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7' + + /* The AWS KMS MRK Aware keyring must be configured with the related CMK. */ + const decryptKeyring = buildAwsKmsMrkAwareStrictMultiKeyringNode({ + generatorKeyId: multiRegionUsWestKey, + }) + + /* Decrypt the data. */ + const { plaintext, messageHeader } = await decrypt(decryptKeyring, result) + + /* Grab the encryption context so you can verify it. */ + const { encryptionContext } = messageHeader + + /* Verify the encryption context. + * If you use an algorithm suite with signing, + * the Encryption SDK adds a name-value pair to the encryption context that contains the public key. + * Because the encryption context might contain additional key-value pairs, + * do not add a test that requires that all key-value pairs match. + * Instead, verify that the key-value pairs you expect match. + */ + Object.entries(context).forEach(([key, value]) => { + if (encryptionContext[key] !== value) + throw new Error('Encryption Context does not match expected values') + }) + + /* Return the values so the code can be tested. */ + return { plaintext, result, cleartext, messageHeader } +} diff --git a/modules/example-node/test/index.test.ts b/modules/example-node/test/index.test.ts index 4f1a78543..c3de1a9d9 100644 --- a/modules/example-node/test/index.test.ts +++ b/modules/example-node/test/index.test.ts @@ -15,6 +15,8 @@ import { kmsEncryptWithMaxEncryptedDataKeysTest, kmsDecryptWithMaxEncryptedDataKeysTest, } from '../src/kms_max_encrypted_data_keys' +import { kmsMultiRegionSimpleTest } from '../src/kms_multi_region_simple' +import { kmsMultiRegionDiscoveryTest } from '../src/kms_multi_region_discovery' import { readFileSync } from 'fs' describe('test', () => { @@ -24,13 +26,13 @@ describe('test', () => { expect(plaintext.toString()).to.equal(cleartext) }) - it('kms', async () => { + it('kms simple', async () => { const { cleartext, plaintext } = await kmsSimpleTest() expect(plaintext.toString()).to.equal(cleartext) }) - it('kms', async () => { + it('kms stream', async () => { const test = await kmsStreamTest(__filename) const clearFile = readFileSync(__filename) @@ -106,4 +108,18 @@ describe('test', () => { 'maxEncryptedDataKeys exceeded.' ) }) + + it('kms multi-Region simple', async () => { + const { cleartext, plaintext } = await kmsMultiRegionSimpleTest() + + expect(plaintext.toString()).to.equal(cleartext) + }) + + it('kms multi-Region discovery', async function () { + this.timeout(3000) + const { result, cleartext } = await kmsMultiRegionSimpleTest() + + const { plaintext } = await kmsMultiRegionDiscoveryTest(result) + expect(plaintext.toString()).to.equal(cleartext) + }) }) diff --git a/modules/integration-browser/karma.conf.js b/modules/integration-browser/karma.conf.js index aa3701c62..c25b6c9ee 100644 --- a/modules/integration-browser/karma.conf.js +++ b/modules/integration-browser/karma.conf.js @@ -18,7 +18,13 @@ module.exports = function (config) { 'fixtures/encrypt_tests.json', 'fixtures/decrypt_oracle.json', '/fixtures/concurrency.json', - { pattern: 'fixtures/*.json', included: false, served: true, watched: false, nocache: true }, + { + pattern: 'fixtures/*.json', + included: false, + served: true, + watched: false, + nocache: true, + }, 'build/module/integration.decrypt.test.js', 'build/module/integration.encrypt.test.js', ], @@ -36,9 +42,12 @@ module.exports = function (config) { colors: true, modules: true, reasons: true, - errorDetails: true + errorDetails: true, + }, + devtool: 'inline-source-map', + node: { + fs: 'empty', }, - devtool: 'inline-source-map' }, plugins: [ 'karma-parallel', @@ -46,7 +55,7 @@ module.exports = function (config) { 'karma-webpack', 'karma-json-fixtures-preprocessor', 'karma-chrome-launcher', - 'karma-jasmine' + 'karma-jasmine', ], reporters: ['progress'], port: 9876, @@ -63,14 +72,14 @@ module.exports = function (config) { '--no-sandbox', '--disable-setuid-sandbox', '--enable-logging', - ] - } + ], + }, }, singleRun: true, concurrency: Infinity, exclude: ['**/*.d.ts'], parallelOptions: { - executors: concurrency - } + executors: concurrency, + }, }) } diff --git a/modules/integration-browser/src/build_decrypt_fixtures.ts b/modules/integration-browser/src/build_decrypt_fixtures.ts index d38dcd0bd..d09c4d758 100644 --- a/modules/integration-browser/src/build_decrypt_fixtures.ts +++ b/modules/integration-browser/src/build_decrypt_fixtures.ts @@ -87,7 +87,10 @@ export async function buildDecryptFixtures( if (!cipherInfo) throw new Error(`no file for ${name}: ${ciphertext}`) const cipherText = await streamToPromise(await cipherInfo.stream()) - const keysInfo = masterKeys.map((keyInfo: { key: string | number }) => { + const keysInfo = masterKeys.map((keyInfo) => { + if (keyInfo.type === 'aws-kms-mrk-aware-discovery') { + return [keyInfo] as KeyInfoTuple + } const key = keys[keyInfo.key] if (!key) throw new Error(`no key for ${name}`) return [keyInfo, key] as KeyInfoTuple diff --git a/modules/integration-browser/src/decrypt_materials_manager_web_crypto.ts b/modules/integration-browser/src/decrypt_materials_manager_web_crypto.ts index c604cf9c5..76f1b095c 100644 --- a/modules/integration-browser/src/decrypt_materials_manager_web_crypto.ts +++ b/modules/integration-browser/src/decrypt_materials_manager_web_crypto.ts @@ -3,6 +3,7 @@ import { needs, + KeyringWebCrypto, MultiKeyringWebCrypto, KmsKeyringBrowser, KmsWebCryptoClientSupplier, @@ -11,15 +12,20 @@ import { WrappingSuiteIdentifier, RawAesWrappingSuiteIdentifier, RawRsaKeyringWebCrypto, + buildAwsKmsMrkAwareStrictMultiKeyringBrowser, + buildAwsKmsMrkAwareDiscoveryMultiKeyringBrowser, } from '@aws-crypto/client-browser' import { RsaKeyInfo, AesKeyInfo, KmsKeyInfo, + KmsMrkAwareKeyInfo, + KmsMrkAwareDiscoveryKeyInfo, RSAKey, AESKey, KMSKey, KeyInfoTuple, + buildGetKeyring, } from '@aws-crypto/integration-vectors' import { fromBase64 } from '@aws-sdk/util-base64-browser' @@ -39,6 +45,14 @@ const Bits2RawAesWrappingSuiteIdentifier: { 256: RawAesWrappingSuiteIdentifier.AES256_GCM_IV12_TAG16_NO_PADDING, } +const keyringWebCrypto = buildGetKeyring>({ + kmsKeyring, + kmsMrkAwareKeyring, + kmsMrkAwareDiscoveryKeyring, + aesKeyring, + rsaKeyring, +}) + export async function encryptMaterialsManagerWebCrypto( keyInfos: KeyInfoTuple[] ): Promise { @@ -55,33 +69,41 @@ export async function decryptMaterialsManagerWebCrypto( return new MultiKeyringWebCrypto({ children }) } -async function keyringWebCrypto([info, key]: KeyInfoTuple) { - if (info.type === 'aws-kms' && key.type === 'aws-kms') { - return kmsKeyring(info, key) - } - if ( - info.type === 'raw' && - info['encryption-algorithm'] === 'aes' && - key.type === 'symmetric' - ) { - return aesKeyring(info, key) - } - if ( - info.type === 'raw' && - info['encryption-algorithm'] === 'rsa' && - (key.type === 'public' || key.type === 'private') - ) { - return rsaKeyring(info, key) +async function kmsKeyring(_keyInfo: KmsKeyInfo, key: KMSKey) { + const generatorKeyId = key['key-id'] + const clientProvider: KmsWebCryptoClientSupplier = (region: string) => { + return new KMS({ region, credentials }) } - throw new Error('Unsupported keyring type') + return new KmsKeyringBrowser({ generatorKeyId, clientProvider }) } -function kmsKeyring(_keyInfo: KmsKeyInfo, key: KMSKey) { +async function kmsMrkAwareKeyring(_keyInfo: KmsMrkAwareKeyInfo, key: KMSKey) { const generatorKeyId = key['key-id'] const clientProvider: KmsWebCryptoClientSupplier = (region: string) => { return new KMS({ region, credentials }) } - return new KmsKeyringBrowser({ generatorKeyId, clientProvider }) + return buildAwsKmsMrkAwareStrictMultiKeyringBrowser({ + generatorKeyId, + clientProvider, + }) +} + +async function kmsMrkAwareDiscoveryKeyring( + keyInfo: KmsMrkAwareDiscoveryKeyInfo +) { + const regions = [keyInfo['default-mrk-region']] + const { 'aws-kms-discovery-filter': filter } = keyInfo + const discoveryFilter = filter + ? { partition: filter.partition, accountIDs: filter['account-ids'] } + : undefined + const clientProvider: KmsWebCryptoClientSupplier = (region: string) => { + return new KMS({ region, credentials }) + } + return buildAwsKmsMrkAwareDiscoveryMultiKeyringBrowser({ + discoveryFilter, + regions, + clientProvider, + }) } async function aesKeyring(keyInfo: AesKeyInfo, key: AESKey) { diff --git a/modules/integration-browser/src/testDecryptFixture.ts b/modules/integration-browser/src/testDecryptFixture.ts index c850da60b..346b1be85 100644 --- a/modules/integration-browser/src/testDecryptFixture.ts +++ b/modules/integration-browser/src/testDecryptFixture.ts @@ -26,22 +26,23 @@ export const notSupportedDecryptMessages = [ // overly permissive DER decoding in asn1.js. // See https://github.com/indutny/asn1.js/pull/128 export const bitFlippedDerTagsVectors = [ - 'c415e987-48ff-4da3-a70a-2fe67c25b700', // Bit 2944 flipped - '470419df-6280-4813-8f53-ab9f2c979dee', // Bit 2945 flipped - '3bf06756-45b2-4fa7-85d2-e636e221da07', // Bit 2946 flipped - 'd4e4cf08-1a0c-48ca-83ae-7f63bd8e8ba4', // Bit 2960 flipped - '114d533b-7ead-4601-b87b-42d25c28c2ef', // Bit 2961 flipped - '8f38fb88-539a-4a53-b9d2-031fc6bfadf7', // Bit 2962 flipped - '32640303-2f79-44a2-83cd-476e22360491', // Bit 3368 flipped - '4e6151b0-8799-4c69-9b1a-6223cfce795d', // Bit 3369 flipped - 'c33a0daa-806e-428c-8558-def65f41817b', // Bit 3370 flipped + '82f59ef1-7002-486d-9679-f4b358e6f05e', // Bit 2944 flipped + '516abc51-d740-4715-8888-73ebf1c3b674', // Bit 2945 flipped + '6911b1b6-238c-4c82-a27c-31a21a770380', // Bit 2946 flipped + '9e5dd64e-fa70-42cf-9f32-7168710ca193', // Bit 2960 flipped + '233e79f7-aab8-4381-9fdc-2b2b4d0efd91', // Bit 2961 flipped + '9905767c-a214-4483-8e69-425a8b1ec833', // Bit 2962 flipped + 'b40cecee-bff8-45c0-9679-a7533ce8ad75', // Bit 3368 flipped + '1f665dd8-08f3-43b0-96ab-deca39634bea', // Bit 3369 flipped + 'f673bdf3-40a8-4551-bc7f-866b289e4d03', // Bit 3370 flipped ] // The signatures on these messages fail to verify due to // a known but yet to be fully diagnosed browser-specific issue. +// The error message is `Error: Invalid Signature` export const unverifiableSignatureVectors = [ - '55c1a27a-70ec-4d3a-8dda-d718eef0a532', - 'c0a92e53-f75b-4168-81ae-3cdd69f8dd0c', + '2ad4430c-1b2e-46b3-a71d-a8e458f28a69', + 'e3b4ce89-a5f4-4194-9bc5-2984cf1d2a88', ] /*The contract for the two test*DecryptFixture methods: diff --git a/modules/integration-node/src/decrypt_materials_manager_node.ts b/modules/integration-node/src/decrypt_materials_manager_node.ts index 2f1c6e27a..eb7005d6a 100644 --- a/modules/integration-node/src/decrypt_materials_manager_node.ts +++ b/modules/integration-node/src/decrypt_materials_manager_node.ts @@ -3,6 +3,7 @@ import { needs, + KeyringNode, MultiKeyringNode, KmsKeyringNode, RawAesKeyringNode, @@ -10,15 +11,20 @@ import { RawAesWrappingSuiteIdentifier, RawRsaKeyringNode, oaepHashSupported, + buildAwsKmsMrkAwareStrictMultiKeyringNode, + buildAwsKmsMrkAwareDiscoveryMultiKeyringNode, } from '@aws-crypto/client-node' import { RsaKeyInfo, AesKeyInfo, KmsKeyInfo, + KmsMrkAwareKeyInfo, + KmsMrkAwareDiscoveryKeyInfo, RSAKey, AESKey, KMSKey, KeyInfoTuple, + buildGetKeyring, } from '@aws-crypto/integration-vectors' import { constants } from 'crypto' @@ -30,6 +36,14 @@ const Bits2RawAesWrappingSuiteIdentifier: { 256: RawAesWrappingSuiteIdentifier.AES256_GCM_IV12_TAG16_NO_PADDING, } +export const keyringNode = buildGetKeyring({ + kmsKeyring, + kmsMrkAwareKeyring, + kmsMrkAwareDiscoveryKeyring, + aesKeyring, + rsaKeyring, +}) + export function encryptMaterialsManagerNode(keyInfos: KeyInfoTuple[]) { const [generator, ...children] = keyInfos.map(keyringNode) return new MultiKeyringNode({ generator, children }) @@ -40,32 +54,30 @@ export function decryptMaterialsManagerNode(keyInfos: KeyInfoTuple[]) { return new MultiKeyringNode({ children }) } -export function keyringNode([info, key]: KeyInfoTuple) { - if (info.type === 'aws-kms' && key.type === 'aws-kms') { - return kmsKeyring(info, key) - } - if ( - info.type === 'raw' && - info['encryption-algorithm'] === 'aes' && - key.type === 'symmetric' - ) { - return aesKeyring(info, key) - } - if ( - info.type === 'raw' && - info['encryption-algorithm'] === 'rsa' && - (key.type === 'public' || key.type === 'private') - ) { - return rsaKeyring(info, key) - } - throw new Error('Unsupported keyring type') -} - export function kmsKeyring(_keyInfo: KmsKeyInfo, key: KMSKey) { const generatorKeyId = key['key-id'] return new KmsKeyringNode({ generatorKeyId }) } +export function kmsMrkAwareKeyring(_keyInfo: KmsMrkAwareKeyInfo, key: KMSKey) { + const generatorKeyId = key['key-id'] + return buildAwsKmsMrkAwareStrictMultiKeyringNode({ generatorKeyId }) +} + +export function kmsMrkAwareDiscoveryKeyring( + keyInfo: KmsMrkAwareDiscoveryKeyInfo +) { + const regions = [keyInfo['default-mrk-region']] + const { 'aws-kms-discovery-filter': filter } = keyInfo + const discoveryFilter = filter + ? { partition: filter.partition, accountIDs: filter['account-ids'] } + : undefined + return buildAwsKmsMrkAwareDiscoveryMultiKeyringNode({ + discoveryFilter, + regions, + }) +} + export function aesKeyring(keyInfo: AesKeyInfo, key: AESKey) { const keyName = key['key-id'] const keyNamespace = keyInfo['provider-id'] diff --git a/modules/integration-vectors/package.json b/modules/integration-vectors/package.json index f665a94ea..ae9f04702 100644 --- a/modules/integration-vectors/package.json +++ b/modules/integration-vectors/package.json @@ -10,7 +10,7 @@ "test": "npm run lint && npm run build && node ./build/main/index.js && npm run mocha" }, "dependencies": { - "@aws-crypto/client-node": "file:../client-node", + "@aws-crypto/material-management": "file:../material-management", "@types/got": "^9.6.9", "@types/stream-to-promise": "^2.2.0", "@types/yauzl": "^2.9.1", diff --git a/modules/integration-vectors/src/build_get_keyring.ts b/modules/integration-vectors/src/build_get_keyring.ts new file mode 100644 index 000000000..9e0012aca --- /dev/null +++ b/modules/integration-vectors/src/build_get_keyring.ts @@ -0,0 +1,62 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + KeyInfoTuple, + RsaKeyInfo, + AesKeyInfo, + KmsKeyInfo, + KmsMrkAwareKeyInfo, + KmsMrkAwareDiscoveryKeyInfo, + RSAKey, + AESKey, + KMSKey, +} from './types' + +export function buildGetKeyring({ + kmsKeyring, + kmsMrkAwareKeyring, + kmsMrkAwareDiscoveryKeyring, + aesKeyring, + rsaKeyring, +}: { + kmsKeyring(keyInfo: KmsKeyInfo, key: KMSKey): K + kmsMrkAwareKeyring(keyInfo: KmsMrkAwareKeyInfo, key: KMSKey): K + kmsMrkAwareDiscoveryKeyring(keyInfo: KmsMrkAwareDiscoveryKeyInfo): K + aesKeyring(keyInfo: AesKeyInfo, key: AESKey): K + rsaKeyring(keyInfo: RsaKeyInfo, key: RSAKey): K +}): (info: KeyInfoTuple) => K { + return function getKeyring([info, key]: KeyInfoTuple): K { + if (info.type === 'aws-kms' && key && key.type === 'aws-kms') { + return kmsKeyring(info, key) + } + + if (info.type === 'aws-kms-mrk-aware' && key && key.type === 'aws-kms') { + return kmsMrkAwareKeyring(info, key) + } + + if (info.type === 'aws-kms-mrk-aware-discovery' && !key) { + return kmsMrkAwareDiscoveryKeyring(info) + } + + if ( + info.type === 'raw' && + info['encryption-algorithm'] === 'aes' && + key && + key.type === 'symmetric' + ) { + return aesKeyring(info, key) + } + + if ( + info.type === 'raw' && + info['encryption-algorithm'] === 'rsa' && + key && + (key.type === 'public' || key.type === 'private') + ) { + return rsaKeyring(info, key) + } + + throw new Error('Unsupported keyring type') + } +} diff --git a/modules/integration-vectors/src/get_decrypt_test_iterator.ts b/modules/integration-vectors/src/get_decrypt_test_iterator.ts index 4e536ad77..3661ee8ed 100644 --- a/modules/integration-vectors/src/get_decrypt_test_iterator.ts +++ b/modules/integration-vectors/src/get_decrypt_test_iterator.ts @@ -72,6 +72,9 @@ export async function _getDecryptTestVectorIterator( const cipherStream = cipherInfo.stream const keysInfo = masterKeys.map((keyInfo) => { + if (keyInfo.type === 'aws-kms-mrk-aware-discovery') { + return [keyInfo] as KeyInfoTuple + } const key = keys[keyInfo.key] if (!key) throw new Error(`no key for ${name}`) return [keyInfo, key] as KeyInfoTuple diff --git a/modules/integration-vectors/src/index.ts b/modules/integration-vectors/src/index.ts index b42bfcf7e..384202dce 100644 --- a/modules/integration-vectors/src/index.ts +++ b/modules/integration-vectors/src/index.ts @@ -3,3 +3,4 @@ export * from './types' export * from './get_decrypt_test_iterator' +export * from './build_get_keyring' diff --git a/modules/integration-vectors/src/types.ts b/modules/integration-vectors/src/types.ts index 5a3cf3117..16150831b 100644 --- a/modules/integration-vectors/src/types.ts +++ b/modules/integration-vectors/src/types.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { EncryptionContext } from '@aws-crypto/client-node' +import { EncryptionContext } from '@aws-crypto/material-management' import { Readable } from 'stream' import { Entry } from 'yauzl' @@ -9,7 +9,13 @@ import { Entry } from 'yauzl' interface BaseDecryptTest { description?: string ciphertext: string - 'master-keys': (RsaKeyInfo | AesKeyInfo | KmsKeyInfo)[] + 'master-keys': ( + | RsaKeyInfo + | AesKeyInfo + | KmsKeyInfo + | KmsMrkAwareKeyInfo + | KmsMrkAwareDiscoveryKeyInfo + )[] 'decryption-method'?: string } @@ -96,7 +102,7 @@ export interface Client { } interface KeyInfo { - type: 'aws-kms' | 'raw' + type: 'aws-kms' | 'raw' | 'aws-kms-mrk-aware' | 'aws-kms-mrk-aware-discovery' key: string } @@ -123,6 +129,20 @@ export interface KmsKeyInfo extends KeyInfo { key: string } +export interface KmsMrkAwareKeyInfo extends KeyInfo { + type: 'aws-kms-mrk-aware' + key: string +} + +export interface KmsMrkAwareDiscoveryKeyInfo { + type: 'aws-kms-mrk-aware-discovery' + 'default-mrk-region': string + 'aws-kms-discovery-filter'?: { + partition: string + 'account-ids': string[] + } +} + interface EncryptTest { plaintext: string algorithm: string @@ -175,3 +195,5 @@ export type KeyInfoTuple = | [RsaKeyInfo, RSAKey] | [AesKeyInfo, AESKey] | [KmsKeyInfo, KMSKey] + | [KmsMrkAwareKeyInfo, KMSKey] + | [KmsMrkAwareDiscoveryKeyInfo] diff --git a/modules/integration-vectors/test/get_decrypt_test_iterator.test.ts b/modules/integration-vectors/test/get_decrypt_test_iterator.test.ts index ab4376768..1a253b579 100644 --- a/modules/integration-vectors/test/get_decrypt_test_iterator.test.ts +++ b/modules/integration-vectors/test/get_decrypt_test_iterator.test.ts @@ -57,6 +57,13 @@ const keyList: KeyList = { encrypt: true, decrypt: false, }, + 'arn:aws:kms-not:us-west-2:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7': { + encrypt: false, + decrypt: false, + type: 'aws-kms', + 'key-id': + 'arn:aws:kms-not:us-west-2:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7', + }, }, } @@ -285,4 +292,43 @@ describe('_getDecryptTestVectorIterator yields Decrypt Tests', () => { '_getDecryptTestVectorIterator should have thrown an error' ) }) + + it('can read an mrk-aware vector', async () => { + const testManifest: DecryptManifestList = { + manifest: decryptManifest, + client: client, + keys: 'file://keys.json', + tests: { + 'e6c7ad4c-5b24-44ce-a65e-62d402239b01': { + ciphertext: ciphertext_path, + 'master-keys': [ + { + type: 'aws-kms-mrk-aware', + key: + 'arn:aws:kms-not:us-west-2:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7', + }, + ], + result: { + error: { + 'error-description': + 'Incorrect encrypted data key provider info: aws:kms:us-west-2:658956600833:key:mrk-80bd8ecdcd4342aebd84b7dc9da498a7', + }, + }, + }, + }, + } + const testIterator = await _getDecryptTestVectorIterator( + mockReadManifest( + testManifest, + ciphertext_path, + cipherStream, + keyList, + plaintext_path, + plainTextStream + ) + ) + + const { value } = testIterator.next() + expect(value.keysInfo[0][0].type, 'aws-kms-mrk-aware') + }) }) diff --git a/modules/kms-keyring-browser/src/index.ts b/modules/kms-keyring-browser/src/index.ts index 3e9cfe854..882e1930b 100644 --- a/modules/kms-keyring-browser/src/index.ts +++ b/modules/kms-keyring-browser/src/index.ts @@ -2,3 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 export * from './kms_keyring_browser' +export * from './kms_mrk_keyring_browser' +export * from './kms_mrk_discovery_keyring_browser' +export * from './kms_mrk_strict_multi_keyring_browser' +export * from './kms_mrk_discovery_multi_keyring_browser' diff --git a/modules/kms-keyring-browser/src/kms_keyring_browser.ts b/modules/kms-keyring-browser/src/kms_keyring_browser.ts index b353710a1..e76ea052d 100644 --- a/modules/kms-keyring-browser/src/kms_keyring_browser.ts +++ b/modules/kms-keyring-browser/src/kms_keyring_browser.ts @@ -3,7 +3,6 @@ import { KmsKeyringClass, - KeyRingConstructible, KmsKeyringInput, KMSConstructible, KmsClientSupplier, @@ -21,6 +20,7 @@ import { importForWebCryptoEncryptionMaterial, importForWebCryptoDecryptionMaterial, KeyringWebCrypto, + Newable, } from '@aws-crypto/material-management-browser' import { KMS } from 'aws-sdk' import { version } from './version' @@ -36,9 +36,10 @@ export type KMSWebCryptoConstructible = KMSConstructible< > export type KmsWebCryptoClientSupplier = KmsClientSupplier -export class KmsKeyringBrowser extends KmsKeyringClass( - KeyringWebCrypto as KeyRingConstructible -) { +export class KmsKeyringBrowser extends KmsKeyringClass< + WebCryptoAlgorithmSuite, + KMS +>(KeyringWebCrypto as Newable) { constructor({ clientProvider = cacheKmsClients, keyIds, @@ -69,6 +70,7 @@ immutableClass(KmsKeyringBrowser) export { getClient, cacheKmsClients, + getKmsClient, limitRegions, excludeRegions, cacheClients, diff --git a/modules/kms-keyring-browser/src/kms_mrk_discovery_keyring_browser.ts b/modules/kms-keyring-browser/src/kms_mrk_discovery_keyring_browser.ts new file mode 100644 index 000000000..e4320402c --- /dev/null +++ b/modules/kms-keyring-browser/src/kms_mrk_discovery_keyring_browser.ts @@ -0,0 +1,54 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + AwsKmsMrkAwareSymmetricDiscoveryKeyringClass, + AwsKmsMrkAwareSymmetricDiscoveryKeyringInput, +} from '@aws-crypto/kms-keyring' +import { + WebCryptoAlgorithmSuite, + WebCryptoEncryptionMaterial, + WebCryptoDecryptionMaterial, + EncryptedDataKey, + immutableClass, + importForWebCryptoEncryptionMaterial, + importForWebCryptoDecryptionMaterial, + KeyringWebCrypto, + Newable, +} from '@aws-crypto/material-management-browser' +import { KMS } from 'aws-sdk' + +export type AwsKmsMrkAwareSymmetricDiscoveryKeyringWebCryptoInput = AwsKmsMrkAwareSymmetricDiscoveryKeyringInput + +export class AwsKmsMrkAwareSymmetricDiscoveryKeyringBrowser extends AwsKmsMrkAwareSymmetricDiscoveryKeyringClass< + WebCryptoAlgorithmSuite, + KMS +>(KeyringWebCrypto as Newable) { + readonly client!: KMS + + constructor({ + client, + discoveryFilter, + grantTokens, + }: AwsKmsMrkAwareSymmetricDiscoveryKeyringWebCryptoInput) { + super({ client, discoveryFilter, grantTokens }) + } + + async _onEncrypt( + material: WebCryptoEncryptionMaterial + ): Promise { + const _material = await super._onEncrypt(material) + + return importForWebCryptoEncryptionMaterial(_material) + } + + async _onDecrypt( + material: WebCryptoDecryptionMaterial, + encryptedDataKeys: EncryptedDataKey[] + ): Promise { + const _material = await super._onDecrypt(material, encryptedDataKeys) + + return importForWebCryptoDecryptionMaterial(_material) + } +} +immutableClass(AwsKmsMrkAwareSymmetricDiscoveryKeyringBrowser) diff --git a/modules/kms-keyring-browser/src/kms_mrk_discovery_multi_keyring_browser.ts b/modules/kms-keyring-browser/src/kms_mrk_discovery_multi_keyring_browser.ts new file mode 100644 index 000000000..2d4a0d57e --- /dev/null +++ b/modules/kms-keyring-browser/src/kms_mrk_discovery_multi_keyring_browser.ts @@ -0,0 +1,20 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { getAwsKmsMrkAwareDiscoveryMultiKeyringBuilder } from '@aws-crypto/kms-keyring' +import { + MultiKeyringWebCrypto, + WebCryptoAlgorithmSuite, +} from '@aws-crypto/material-management-browser' +import { getKmsClient } from '.' +import { AwsKmsMrkAwareSymmetricDiscoveryKeyringBrowser } from './kms_mrk_discovery_keyring_browser' +import { KMS } from 'aws-sdk' + +export const buildAwsKmsMrkAwareDiscoveryMultiKeyringBrowser = getAwsKmsMrkAwareDiscoveryMultiKeyringBuilder< + WebCryptoAlgorithmSuite, + KMS +>( + AwsKmsMrkAwareSymmetricDiscoveryKeyringBrowser, + MultiKeyringWebCrypto, + getKmsClient +) diff --git a/modules/kms-keyring-browser/src/kms_mrk_keyring_browser.ts b/modules/kms-keyring-browser/src/kms_mrk_keyring_browser.ts new file mode 100644 index 000000000..c66295581 --- /dev/null +++ b/modules/kms-keyring-browser/src/kms_mrk_keyring_browser.ts @@ -0,0 +1,56 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + AwsKmsMrkAwareSymmetricKeyringClass, + AwsKmsMrkAwareSymmetricKeyringInput, +} from '@aws-crypto/kms-keyring' +import { + WebCryptoAlgorithmSuite, + WebCryptoEncryptionMaterial, + WebCryptoDecryptionMaterial, + EncryptedDataKey, + immutableClass, + importForWebCryptoEncryptionMaterial, + importForWebCryptoDecryptionMaterial, + KeyringWebCrypto, + Newable, +} from '@aws-crypto/material-management-browser' +import { KMS } from 'aws-sdk' + +export type AwsKmsMrkAwareSymmetricKeyringWebCryptoInput = AwsKmsMrkAwareSymmetricKeyringInput + +export class AwsKmsMrkAwareSymmetricKeyringBrowser extends AwsKmsMrkAwareSymmetricKeyringClass< + WebCryptoAlgorithmSuite, + KMS +>(KeyringWebCrypto as Newable) { + readonly client!: KMS + readonly keyId!: string + readonly grantTokens?: string[] + + constructor({ + client, + keyId, + grantTokens, + }: AwsKmsMrkAwareSymmetricKeyringWebCryptoInput) { + super({ client, keyId, grantTokens }) + } + + async _onEncrypt( + material: WebCryptoEncryptionMaterial + ): Promise { + const _material = await super._onEncrypt(material) + + return importForWebCryptoEncryptionMaterial(_material) + } + + async _onDecrypt( + material: WebCryptoDecryptionMaterial, + encryptedDataKeys: EncryptedDataKey[] + ): Promise { + const _material = await super._onDecrypt(material, encryptedDataKeys) + + return importForWebCryptoDecryptionMaterial(_material) + } +} +immutableClass(AwsKmsMrkAwareSymmetricKeyringBrowser) diff --git a/modules/kms-keyring-browser/src/kms_mrk_strict_multi_keyring_browser.ts b/modules/kms-keyring-browser/src/kms_mrk_strict_multi_keyring_browser.ts new file mode 100644 index 000000000..5d0b1e40c --- /dev/null +++ b/modules/kms-keyring-browser/src/kms_mrk_strict_multi_keyring_browser.ts @@ -0,0 +1,16 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { getAwsKmsMrkAwareStrictMultiKeyringBuilder } from '@aws-crypto/kms-keyring' +import { + MultiKeyringWebCrypto, + WebCryptoAlgorithmSuite, +} from '@aws-crypto/material-management-browser' +import { getKmsClient } from '.' +import { AwsKmsMrkAwareSymmetricKeyringBrowser } from './kms_mrk_keyring_browser' +import { KMS } from 'aws-sdk' + +export const buildAwsKmsMrkAwareStrictMultiKeyringBrowser = getAwsKmsMrkAwareStrictMultiKeyringBuilder< + WebCryptoAlgorithmSuite, + KMS +>(AwsKmsMrkAwareSymmetricKeyringBrowser, MultiKeyringWebCrypto, getKmsClient) diff --git a/modules/kms-keyring-browser/test/kms_mrk_discovery_keyring_browser.test.ts b/modules/kms-keyring-browser/test/kms_mrk_discovery_keyring_browser.test.ts new file mode 100644 index 000000000..a68214be6 --- /dev/null +++ b/modules/kms-keyring-browser/test/kms_mrk_discovery_keyring_browser.test.ts @@ -0,0 +1,105 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +/* eslint-env mocha */ + +import * as chai from 'chai' +import chaiAsPromised from 'chai-as-promised' +import { + AwsKmsMrkAwareSymmetricDiscoveryKeyringBrowser, + AwsKmsMrkAwareSymmetricKeyringBrowser, +} from '../src/index' +import { + KeyringWebCrypto, + WebCryptoEncryptionMaterial, + WebCryptoAlgorithmSuite, + AlgorithmSuiteIdentifier, + WebCryptoDecryptionMaterial, +} from '@aws-crypto/material-management-browser' +import { KMS } from 'aws-sdk' + +chai.use(chaiAsPromised) +const { expect } = chai + +describe('AwsKmsMrkAwareSymmetricDiscoveryKeyringBrowser::constructor', () => { + it('constructor decorates', async () => { + const discoveryFilter = { accountIDs: ['658956600833'], partition: 'aws' } + const grantTokens = ['grant'] + const client: any = { config: { region: 'us-west-2' } } + + const test = new AwsKmsMrkAwareSymmetricDiscoveryKeyringBrowser({ + client, + discoveryFilter, + grantTokens, + }) + + expect(test.discoveryFilter).to.deep.equal(discoveryFilter) + expect(test.client).to.equal(client) + expect(test.grantTokens).to.equal(grantTokens) + }) + + it('instance of KeyringWebCrypto', () => { + const discoveryFilter = { accountIDs: ['658956600833'], partition: 'aws' } + const grantTokens = ['grant'] + const client: any = { config: { region: 'us-west-2' } } + + const test = new AwsKmsMrkAwareSymmetricDiscoveryKeyringBrowser({ + client, + discoveryFilter, + grantTokens, + }) + + expect(test instanceof KeyringWebCrypto).to.equal(true) + }) +}) + +/* Injected from @aws-sdk/karma-credential-loader. */ +declare const credentials: any + +describe('AwsKmsMrkAwareSymmetricKeyringBrowser encrypt/decrypt', () => { + const discoveryFilter = { accountIDs: ['658956600833'], partition: 'aws' } + + const eastKeyId = + 'arn:aws:kms:us-east-1:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7' + const grantTokens = ['grant'] + const encryptionContext = { some: 'context' } + const suite = new WebCryptoAlgorithmSuite( + AlgorithmSuiteIdentifier.ALG_AES256_GCM_IV12_TAG16_HKDF_SHA256 + ) + + const keyring = new AwsKmsMrkAwareSymmetricDiscoveryKeyringBrowser({ + // Note the difference in the region from the keyId + client: new KMS({ region: 'us-west-2', credentials }), + discoveryFilter, + grantTokens, + }) + + it('throws an error on encrypt', async () => { + const material = new WebCryptoEncryptionMaterial(suite, encryptionContext) + return expect(keyring.onEncrypt(material)).to.rejectedWith( + Error, + 'AwsKmsMrkAwareSymmetricDiscoveryKeyring cannot be used to encrypt' + ) + }) + + it('can decrypt an EncryptedDataKey', async () => { + const encryptKeyring = new AwsKmsMrkAwareSymmetricKeyringBrowser({ + client: new KMS({ region: 'us-east-1', credentials }), + keyId: eastKeyId, + grantTokens, + }) + const encryptMaterial = await encryptKeyring.onEncrypt( + new WebCryptoEncryptionMaterial(suite, encryptionContext) + ) + const [edk] = encryptMaterial.encryptedDataKeys + + const material = await keyring.onDecrypt( + new WebCryptoDecryptionMaterial(suite, encryptionContext), + [edk] + ) + const test = await keyring.onDecrypt(material, [edk]) + expect(test.hasValidKey()).to.equal(true) + // The UnencryptedDataKey should be zeroed, because the cryptoKey has been set + expect(() => test.getUnencryptedDataKey()).to.throw() + }) +}) diff --git a/modules/kms-keyring-browser/test/kms_mrk_keyring_browser.test.ts b/modules/kms-keyring-browser/test/kms_mrk_keyring_browser.test.ts new file mode 100644 index 000000000..4a919c1f8 --- /dev/null +++ b/modules/kms-keyring-browser/test/kms_mrk_keyring_browser.test.ts @@ -0,0 +1,114 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +/* eslint-env mocha */ + +import * as chai from 'chai' +import chaiAsPromised from 'chai-as-promised' +import { AwsKmsMrkAwareSymmetricKeyringBrowser } from '../src/index' +import { + KeyringWebCrypto, + WebCryptoEncryptionMaterial, + WebCryptoAlgorithmSuite, + AlgorithmSuiteIdentifier, + EncryptedDataKey, + WebCryptoDecryptionMaterial, + KeyringTraceFlag, +} from '@aws-crypto/material-management-browser' +import { KMS } from 'aws-sdk' + +chai.use(chaiAsPromised) +const { expect } = chai + +describe('AwsKmsMrkAwareSymmetricKeyringBrowser::constructor', () => { + const keyId = + 'arn:aws:kms:us-west-2:658956600833:key/mrk-b3537ef1-d8dc-4780-9f5a-55776cbb2f7f' + const grantTokens = ['grant'] + const client: any = {} + it('constructor decorates', async () => { + const test = new AwsKmsMrkAwareSymmetricKeyringBrowser({ + client, + keyId, + grantTokens, + }) + + expect(test.keyId).to.equal(keyId) + expect(test.client).to.equal(client) + expect(test.grantTokens).to.equal(grantTokens) + }) + + it('instance of KeyringWebCrypto', () => { + const test = new AwsKmsMrkAwareSymmetricKeyringBrowser({ + client, + keyId, + grantTokens, + }) + expect(test instanceof KeyringWebCrypto).to.equal(true) + }) +}) + +/* Injected from @aws-sdk/karma-credential-loader. */ +declare const credentials: any + +describe('AwsKmsMrkAwareSymmetricKeyringBrowser encrypt/decrypt', () => { + const westKeyId = + 'arn:aws:kms:us-west-2:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7' + const eastKeyId = + 'arn:aws:kms:us-east-1:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7' + const grantTokens = ['grant'] + const encryptionContext = { some: 'context' } + const suite = new WebCryptoAlgorithmSuite( + AlgorithmSuiteIdentifier.ALG_AES256_GCM_IV12_TAG16_HKDF_SHA256 + ) + + const encryptKeyring = new AwsKmsMrkAwareSymmetricKeyringBrowser({ + client: new KMS({ region: 'us-west-2', credentials }), + keyId: westKeyId, + grantTokens, + }) + const decryptKeyring = new AwsKmsMrkAwareSymmetricKeyringBrowser({ + client: new KMS({ region: 'us-east-1', credentials }), + keyId: eastKeyId, + grantTokens, + }) + let encryptedDataKey: EncryptedDataKey + + it('can encrypt and create unencrypted data key', async () => { + const material = new WebCryptoEncryptionMaterial(suite, encryptionContext) + const test = await encryptKeyring.onEncrypt(material) + expect(test.hasValidKey()).to.equal(true) + const udk = test.getUnencryptedDataKey() + expect(udk).to.have.lengthOf(suite.keyLengthBytes) + expect(test.encryptedDataKeys).to.have.lengthOf(1) + const [edk] = test.encryptedDataKeys + encryptedDataKey = edk + }) + + it('can encrypt a pre-existing plaintext data key', async () => { + const seedMaterial = new WebCryptoEncryptionMaterial( + suite, + encryptionContext + ).setUnencryptedDataKey(new Uint8Array(suite.keyLengthBytes), { + keyName: 'keyName', + keyNamespace: 'keyNamespace', + flags: KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY, + }) + const encryptTest = await encryptKeyring.onEncrypt(seedMaterial) + expect(encryptTest.hasValidKey()).to.equal(true) + expect(encryptTest.encryptedDataKeys).to.have.lengthOf(1) + const [kmsEDK] = encryptTest.encryptedDataKeys + expect(kmsEDK.providerId).to.equal('aws-kms') + expect(kmsEDK.providerInfo).to.equal(westKeyId) + }) + + it('can decrypt an EncryptedDataKey', async () => { + const suite = new WebCryptoAlgorithmSuite( + AlgorithmSuiteIdentifier.ALG_AES256_GCM_IV12_TAG16_HKDF_SHA256 + ) + const material = new WebCryptoDecryptionMaterial(suite, encryptionContext) + const test = await decryptKeyring.onDecrypt(material, [encryptedDataKey]) + expect(test.hasValidKey()).to.equal(true) + // The UnencryptedDataKey should be zeroed, because the cryptoKey has been set + expect(() => test.getUnencryptedDataKey()).to.throw() + }) +}) diff --git a/modules/kms-keyring-node/src/index.ts b/modules/kms-keyring-node/src/index.ts index ec49cca12..d6fcd98dc 100644 --- a/modules/kms-keyring-node/src/index.ts +++ b/modules/kms-keyring-node/src/index.ts @@ -2,3 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 export * from './kms_keyring_node' +export * from './kms_mrk_keyring_node' +export * from './kms_mrk_discovery_keyring_node' +export * from './kms_mrk_strict_multi_keyring_node' +export * from './kms_mrk_discovery_multi_keyring_node' diff --git a/modules/kms-keyring-node/src/kms_keyring_node.ts b/modules/kms-keyring-node/src/kms_keyring_node.ts index dec1e20d9..86b72329c 100644 --- a/modules/kms-keyring-node/src/kms_keyring_node.ts +++ b/modules/kms-keyring-node/src/kms_keyring_node.ts @@ -3,7 +3,6 @@ import { KmsKeyringClass, - KeyRingConstructible, KmsKeyringInput, KMSConstructible, KmsClientSupplier, @@ -13,9 +12,10 @@ import { cacheClients, } from '@aws-crypto/kms-keyring' import { - NodeAlgorithmSuite, immutableClass, KeyringNode, + Newable, + NodeAlgorithmSuite, } from '@aws-crypto/material-management-node' import { KMS } from 'aws-sdk' import { version } from './version' @@ -31,8 +31,8 @@ export type KMSNodeConstructible = KMSConstructible< > export type KmsNodeClientSupplier = KmsClientSupplier -export class KmsKeyringNode extends KmsKeyringClass( - KeyringNode as KeyRingConstructible +export class KmsKeyringNode extends KmsKeyringClass( + KeyringNode as Newable ) { constructor({ clientProvider = cacheKmsClients, diff --git a/modules/kms-keyring-node/src/kms_mrk_discovery_keyring_node.ts b/modules/kms-keyring-node/src/kms_mrk_discovery_keyring_node.ts new file mode 100644 index 000000000..0055edb52 --- /dev/null +++ b/modules/kms-keyring-node/src/kms_mrk_discovery_keyring_node.ts @@ -0,0 +1,20 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + AwsKmsMrkAwareSymmetricDiscoveryKeyringClass, + AwsKmsMrkAwareSymmetricDiscoveryKeyringInput, +} from '@aws-crypto/kms-keyring' +import { + KeyringNode, + Newable, + NodeAlgorithmSuite, +} from '@aws-crypto/material-management-node' +import { KMS } from 'aws-sdk' + +export type AwsKmsMrkAwareSymmetricDiscoveryKeyringNodeInput = AwsKmsMrkAwareSymmetricDiscoveryKeyringInput + +export const AwsKmsMrkAwareSymmetricDiscoveryKeyringNode = AwsKmsMrkAwareSymmetricDiscoveryKeyringClass< + NodeAlgorithmSuite, + KMS +>(KeyringNode as Newable) diff --git a/modules/kms-keyring-node/src/kms_mrk_discovery_multi_keyring_node.ts b/modules/kms-keyring-node/src/kms_mrk_discovery_multi_keyring_node.ts new file mode 100644 index 000000000..538fbb777 --- /dev/null +++ b/modules/kms-keyring-node/src/kms_mrk_discovery_multi_keyring_node.ts @@ -0,0 +1,29 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + getAwsKmsMrkAwareDiscoveryMultiKeyringBuilder, + KmsClientSupplier, +} from '@aws-crypto/kms-keyring' +import { + MultiKeyringNode, + NodeAlgorithmSuite, +} from '@aws-crypto/material-management' +import { getKmsClient } from '.' +import { AwsKmsMrkAwareSymmetricDiscoveryKeyringNode } from './kms_mrk_discovery_keyring_node' +import { KMS } from 'aws-sdk' + +export interface AwsKmsMrkAwareDiscoveryMultiKeyringNodeInput { + regions: string[] + clientProvider?: KmsClientSupplier + discoveryFilter?: Readonly<{ + accountIDs: readonly string[] + partition: string + }> + grantTokens?: string[] +} + +export const buildAwsKmsMrkAwareDiscoveryMultiKeyringNode = getAwsKmsMrkAwareDiscoveryMultiKeyringBuilder< + NodeAlgorithmSuite, + KMS +>(AwsKmsMrkAwareSymmetricDiscoveryKeyringNode, MultiKeyringNode, getKmsClient) diff --git a/modules/kms-keyring-node/src/kms_mrk_keyring_node.ts b/modules/kms-keyring-node/src/kms_mrk_keyring_node.ts new file mode 100644 index 000000000..169b7c926 --- /dev/null +++ b/modules/kms-keyring-node/src/kms_mrk_keyring_node.ts @@ -0,0 +1,20 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + AwsKmsMrkAwareSymmetricKeyringClass, + AwsKmsMrkAwareSymmetricKeyringInput, +} from '@aws-crypto/kms-keyring' +import { + KeyringNode, + Newable, + NodeAlgorithmSuite, +} from '@aws-crypto/material-management-node' +import { KMS } from 'aws-sdk' + +export type AwsKmsMrkAwareSymmetricKeyringNodeInput = AwsKmsMrkAwareSymmetricKeyringInput + +export const AwsKmsMrkAwareSymmetricKeyringNode = AwsKmsMrkAwareSymmetricKeyringClass< + NodeAlgorithmSuite, + KMS +>(KeyringNode as Newable) diff --git a/modules/kms-keyring-node/src/kms_mrk_strict_multi_keyring_node.ts b/modules/kms-keyring-node/src/kms_mrk_strict_multi_keyring_node.ts new file mode 100644 index 000000000..37211276e --- /dev/null +++ b/modules/kms-keyring-node/src/kms_mrk_strict_multi_keyring_node.ts @@ -0,0 +1,24 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { getAwsKmsMrkAwareStrictMultiKeyringBuilder } from '@aws-crypto/kms-keyring' +import { KmsClientSupplier } from '@aws-crypto/kms-keyring' +import { + MultiKeyringNode, + NodeAlgorithmSuite, +} from '@aws-crypto/material-management' +import { getKmsClient } from '.' +import { AwsKmsMrkAwareSymmetricKeyringNode } from './kms_mrk_keyring_node' +import { KMS } from 'aws-sdk' + +export interface AwsKmsMrkAwareStrictMultiKeyringNodeInput { + clientProvider?: KmsClientSupplier + generatorKeyId?: string + keyIds?: string[] + grantTokens?: string[] +} + +export const buildAwsKmsMrkAwareStrictMultiKeyringNode = getAwsKmsMrkAwareStrictMultiKeyringBuilder< + NodeAlgorithmSuite, + KMS +>(AwsKmsMrkAwareSymmetricKeyringNode, MultiKeyringNode, getKmsClient) diff --git a/modules/kms-keyring-node/test/kms_mrk_discovery_keyring_node.test.ts b/modules/kms-keyring-node/test/kms_mrk_discovery_keyring_node.test.ts new file mode 100644 index 000000000..b1e4c2b9d --- /dev/null +++ b/modules/kms-keyring-node/test/kms_mrk_discovery_keyring_node.test.ts @@ -0,0 +1,101 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +/* eslint-env mocha */ + +import chai from 'chai' +import chaiAsPromised from 'chai-as-promised' +import { AwsKmsMrkAwareSymmetricDiscoveryKeyringNode } from '../src/index' +import { + KeyringNode, + NodeEncryptionMaterial, + NodeAlgorithmSuite, + AlgorithmSuiteIdentifier, + EncryptedDataKey, + NodeDecryptionMaterial, + needs, +} from '@aws-crypto/material-management-node' +chai.use(chaiAsPromised) +const { expect } = chai +import { KMS } from 'aws-sdk' + +describe('AwsKmsMrkAwareSymmetricKeyringNode::constructor', () => { + it('constructor decorates', async () => { + const discoveryFilter = { accountIDs: ['658956600833'], partition: 'aws' } + const grantTokens = ['grant'] + const client: any = { config: { region: 'us-west-2' } } + + const test = new AwsKmsMrkAwareSymmetricDiscoveryKeyringNode({ + client, + discoveryFilter, + grantTokens, + }) + + expect(test.discoveryFilter).to.deep.equal(discoveryFilter) + expect(test.client).to.equal(client) + expect(test.grantTokens).to.equal(grantTokens) + }) + + it('instance of KeyringNode', () => { + const discoveryFilter = { accountIDs: ['658956600833'], partition: 'aws' } + const grantTokens = ['grant'] + const client: any = { config: { region: 'us-west-2' } } + + const test = new AwsKmsMrkAwareSymmetricDiscoveryKeyringNode({ + client, + discoveryFilter, + grantTokens, + }) + + expect(test instanceof KeyringNode).to.equal(true) + }) +}) + +describe('AwsKmsMrkAwareSymmetricDiscoveryKeyringNode encrypt/decrypt', () => { + const discoveryFilter = { accountIDs: ['658956600833'], partition: 'aws' } + const keyId = + 'arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f' + const grantTokens = ['grant'] + const encryptionContext = { some: 'context' } + const suite = new NodeAlgorithmSuite( + AlgorithmSuiteIdentifier.ALG_AES256_GCM_IV12_TAG16_HKDF_SHA256 + ) + const client = new KMS({ region: 'us-west-2' }) + + const keyring = new AwsKmsMrkAwareSymmetricDiscoveryKeyringNode({ + client, + discoveryFilter, + grantTokens, + }) + + it('throws an error on encrypt', async () => { + const material = new NodeEncryptionMaterial(suite, encryptionContext) + await expect(keyring.onEncrypt(material)).to.rejectedWith( + Error, + 'AwsKmsMrkAwareSymmetricDiscoveryKeyring cannot be used to encrypt' + ) + }) + + it('can decrypt an EncryptedDataKey', async () => { + const { CiphertextBlob } = await client + .generateDataKey({ + KeyId: keyId, + NumberOfBytes: suite.keyLengthBytes, + EncryptionContext: encryptionContext, + }) + .promise() + needs(Buffer.isBuffer(CiphertextBlob), 'never') + const edk = new EncryptedDataKey({ + providerId: 'aws-kms', + providerInfo: keyId, + encryptedDataKey: new Uint8Array(CiphertextBlob), + }) + + const material = await keyring.onDecrypt( + new NodeDecryptionMaterial(suite, encryptionContext), + [edk] + ) + const decryptTest = await keyring.onDecrypt(material, [edk]) + expect(decryptTest.hasValidKey()).to.equal(true) + }) +}) diff --git a/modules/kms-keyring-node/test/kms_mrk_keyring_node.test.ts b/modules/kms-keyring-node/test/kms_mrk_keyring_node.test.ts new file mode 100644 index 000000000..011335cb0 --- /dev/null +++ b/modules/kms-keyring-node/test/kms_mrk_keyring_node.test.ts @@ -0,0 +1,121 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +/* eslint-env mocha */ + +import * as chai from 'chai' +import chaiAsPromised from 'chai-as-promised' +import { AwsKmsMrkAwareSymmetricKeyringNode } from '../src/index' +import { + KeyringNode, + NodeEncryptionMaterial, + NodeAlgorithmSuite, + AlgorithmSuiteIdentifier, + EncryptedDataKey, + NodeDecryptionMaterial, + unwrapDataKey, + KeyringTraceFlag, +} from '@aws-crypto/material-management-node' +import { KMS } from 'aws-sdk' + +chai.use(chaiAsPromised) +const { expect } = chai + +describe('AwsKmsMrkAwareSymmetricKeyringNode::constructor', () => { + it('constructor decorates', async () => { + const keyId = + 'arn:aws:kms:us-east-1:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7' + const grantTokens = ['grant'] + const client: any = {} + + const test = new AwsKmsMrkAwareSymmetricKeyringNode({ + client, + keyId, + grantTokens, + }) + + expect(test.keyId).to.equal(keyId) + expect(test.client).to.equal(client) + expect(test.grantTokens).to.equal(grantTokens) + }) + + it('instance of KeyringNode', () => { + const keyId = + 'arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f' + const grantTokens = ['grant'] + const client: any = {} + + const test = new AwsKmsMrkAwareSymmetricKeyringNode({ + client, + keyId, + grantTokens, + }) + + expect(test instanceof KeyringNode).to.equal(true) + }) +}) + +describe('AwsKmsMrkAwareSymmetricKeyringNode encrypt/decrypt', () => { + const westKeyId = + 'arn:aws:kms:us-west-2:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7' + const eastKeyId = + 'arn:aws:kms:us-east-1:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7' + const grantTokens = ['grant'] + const encryptionContext = { some: 'context' } + const suite = new NodeAlgorithmSuite( + AlgorithmSuiteIdentifier.ALG_AES256_GCM_IV12_TAG16_HKDF_SHA256 + ) + + const encryptKeyring = new AwsKmsMrkAwareSymmetricKeyringNode({ + client: new KMS({ region: 'us-west-2' }), + keyId: westKeyId, + grantTokens, + }) + + const decryptKeyring = new AwsKmsMrkAwareSymmetricKeyringNode({ + client: new KMS({ region: 'us-east-1' }), + keyId: eastKeyId, + grantTokens, + }) + let encryptedDataKey: EncryptedDataKey + let udk: Uint8Array + + it('can encrypt and create unencrypted data key', async () => { + const material = new NodeEncryptionMaterial(suite, encryptionContext) + const encryptTest = await encryptKeyring.onEncrypt(material) + expect(encryptTest.hasValidKey()).to.equal(true) + udk = unwrapDataKey(encryptTest.getUnencryptedDataKey()) + expect(udk).to.have.lengthOf(suite.keyLengthBytes) + expect(encryptTest.encryptedDataKeys).to.have.lengthOf(1) + const [edk] = encryptTest.encryptedDataKeys + encryptedDataKey = edk + }) + + it('can encrypt a pre-existing plaintext data key', async () => { + const seedMaterial = new NodeEncryptionMaterial( + suite, + encryptionContext + ).setUnencryptedDataKey(new Uint8Array(suite.keyLengthBytes), { + keyName: 'keyName', + keyNamespace: 'keyNamespace', + flags: KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY, + }) + const encryptTest = await encryptKeyring.onEncrypt(seedMaterial) + expect(encryptTest.hasValidKey()).to.equal(true) + expect(encryptTest.encryptedDataKeys).to.have.lengthOf(1) + const [kmsEDK] = encryptTest.encryptedDataKeys + expect(kmsEDK.providerId).to.equal('aws-kms') + expect(kmsEDK.providerInfo).to.equal(westKeyId) + }) + + it('can decrypt an EncryptedDataKey', async () => { + const material = new NodeDecryptionMaterial(suite, encryptionContext) + const decryptTest = await decryptKeyring.onDecrypt(material, [ + encryptedDataKey, + ]) + expect(decryptTest.hasValidKey()).to.equal(true) + expect(unwrapDataKey(decryptTest.getUnencryptedDataKey())).to.deep.equal( + udk + ) + }) +}) diff --git a/modules/kms-keyring/src/arn_parsing.ts b/modules/kms-keyring/src/arn_parsing.ts new file mode 100644 index 000000000..191a89a99 --- /dev/null +++ b/modules/kms-keyring/src/arn_parsing.ts @@ -0,0 +1,274 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { needs } from '@aws-crypto/material-management' + +/* See: https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html#arn-syntax-kms + * regex to match: 'resourceType/resourceId' || 'resourceType' + * This is complicated because the `split(':')`. + * The valid resourceType resourceId delimiters are `/`, `:`. + * This means if the delimiter is a `:` it will be split out, + * when splitting the whole arn. + */ +export const KMS_SERVICE = 'kms' + +export type ParsedAwsKmsKeyArn = { + Partition: string + Region: string + AccountId: string + ResourceType: string + ResourceId: string +} + +const ARN_PREFIX = 'arn' +const KEY_RESOURCE_TYPE = 'key' +const ALIAS_RESOURCE_TYPE = 'alias' +const MRK_RESOURCE_ID_PREFIX = 'mrk-' + +const VALID_RESOURCE_TYPES = [KEY_RESOURCE_TYPE, ALIAS_RESOURCE_TYPE] + +/** + * Returns a parsed ARN if a valid AWS KMS Key ARN. + * If the request is a valid resource the function + * will return false. + * However if the ARN is malformed this function throws an error, + */ +export function parseAwsKmsKeyArn( + kmsKeyArn: string +): ParsedAwsKmsKeyArn | false { + /* Precondition: A KMS Key Id must be a non-null string. */ + needs( + kmsKeyArn && typeof kmsKeyArn === 'string', + 'KMS key arn must be a non-null string.' + ) + + const parts = kmsKeyArn.split(':') + + /* Check for early return (Postcondition): A valid ARN has 6 parts. */ + if (parts.length === 1) { + /* Exceptional Postcondition: Only a valid AWS KMS resource. + * This may result in this function being called twice. + * However this is the most correct behavior. + */ + parseAwsKmsResource(kmsKeyArn) + return false + } + + /* See: https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html#arn-syntax-kms + * arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012 + * arn:aws:kms:us-east-1:123456789012:alias/example-alias + */ + const [ + arnLiteral, + partition, + service, + region = '', + account = '', + resource = '', + ] = parts + + const [resourceType, ...resourceSection] = resource.split('/') + + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 + //# The resource section MUST be non-empty and MUST be split by a + //# single "/" any additional "/" are included in the resource id + const resourceId = resourceSection.join('/') + + /* If this is a valid AWS KMS Key ARN, return the parsed ARN */ + needs( + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 + //# MUST start with string "arn" + arnLiteral === ARN_PREFIX && + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 + //# The partition MUST be a non-empty + partition && + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 + //# The service MUST be the string "kms" + service === KMS_SERVICE && + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 + //# The region MUST be a non-empty string + region && + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 + //# The account MUST be a non-empty string + account && + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 + //# The resource type MUST be either "alias" or "key" + VALID_RESOURCE_TYPES.includes(resourceType) && + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 + //# The resource id MUST be a non-empty string + resourceId, + 'Malformed arn.' + ) + return { + Partition: partition, + Region: region, + AccountId: account, + ResourceType: resourceType, + ResourceId: resourceId, + } +} + +export function getRegionFromIdentifier(kmsKeyIdentifier: string): string { + const awsKmsKeyArn = parseAwsKmsKeyArn(kmsKeyIdentifier) + return awsKmsKeyArn ? awsKmsKeyArn.Region : '' +} + +export function parseAwsKmsResource( + resource: string +): Pick { + /* Precondition: An AWS KMS resource can not have a `:`. + * That would make it an ARNlike. + */ + needs(resource.split(':').length === 1, 'Malformed resource.') + + /* `/` is a valid values in an AWS KMS Alias name. */ + const [head, ...tail] = resource.split('/') + + /* Precondition: A raw identifer is only an alias or a key. */ + needs(head === ALIAS_RESOURCE_TYPE || !tail.length, 'Malformed resource.') + + const [resourceType, resourceId] = + head === ALIAS_RESOURCE_TYPE + ? [ALIAS_RESOURCE_TYPE, tail.join('/')] + : [KEY_RESOURCE_TYPE, head] + + return { + ResourceType: resourceType, + ResourceId: resourceId, + } +} + +export function validAwsKmsIdentifier( + kmsKeyIdentifier: string +): + | ParsedAwsKmsKeyArn + | Pick { + return ( + parseAwsKmsKeyArn(kmsKeyIdentifier) || parseAwsKmsResource(kmsKeyIdentifier) + ) +} + +//= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.8 +//# This function MUST take a single AWS KMS ARN +export function isMultiRegionAwsKmsArn( + kmsIdentifier: string | ParsedAwsKmsKeyArn +): boolean { + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.8 + //# If the input is an invalid AWS KMS ARN this function MUST error. + const awsKmsKeyArn = + typeof kmsIdentifier === 'string' + ? parseAwsKmsKeyArn(kmsIdentifier) + : kmsIdentifier + + /* Precondition: The kmsIdentifier must be an ARN. */ + needs(awsKmsKeyArn, 'AWS KMS identifier is not an ARN') + + const { ResourceType, ResourceId } = awsKmsKeyArn + + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.8 + //# If resource type is "alias", this is an AWS KMS alias ARN and MUST + //# return false. + // + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.8 + //# If resource type is "key" and resource ID starts with + //# "mrk-", this is a AWS KMS multi-Region key ARN and MUST return true. + // + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.8 + //# If resource type is "key" and resource ID does not start with "mrk-", + //# this is a (single-region) AWS KMS key ARN and MUST return false. + return ( + ResourceType === KEY_RESOURCE_TYPE && + ResourceId.startsWith(MRK_RESOURCE_ID_PREFIX) + ) +} + +//= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.9 +//# This function MUST take a single AWS KMS identifier +export function isMultiRegionAwsKmsIdentifier(kmsIdentifier: string): boolean { + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.9 + //# If the input starts with "arn:", this MUST return the output of + //# identifying an an AWS KMS multi-Region ARN (aws-kms-key- + //# arn.md#identifying-an-an-aws-kms-multi-region-arn) called with this + //# input. + if (kmsIdentifier.startsWith('arn:')) { + return isMultiRegionAwsKmsArn(kmsIdentifier) + } else if (kmsIdentifier.startsWith('alias/')) { + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.9 + //# If the input starts with "alias/", this an AWS KMS alias and + //# not a multi-Region key id and MUST return false. + return false + } else if (kmsIdentifier.startsWith(MRK_RESOURCE_ID_PREFIX)) { + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.9 + //# If the input starts + //# with "mrk-", this is a multi-Region key id and MUST return true. + return true + } + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.9 + //# If + //# the input does not start with any of the above, this is not a multi- + //# Region key id and MUST return false. + return false +} + +/* Returns a boolean representing whether two AWS KMS Key IDs should be considered equal. + * For everything except MRK-indicating ARNs, this is a direct comparison. + * For MRK-indicating ARNs, this is a comparison of every ARN component except region. + * Throws an error if the IDs are not explicitly equal and at least one of the IDs + * is not a valid AWS KMS Key ARN or alias name. + */ +//= compliance/framework/aws-kms/aws-kms-mrk-match-for-decrypt.txt#2.5 +//# The caller MUST provide: +export function mrkAwareAwsKmsKeyIdCompare( + keyId1: string, + keyId2: string +): boolean { + //= compliance/framework/aws-kms/aws-kms-mrk-match-for-decrypt.txt#2.5 + //# If both identifiers are identical, this function MUST return "true". + if (keyId1 === keyId2) return true + + //= compliance/framework/aws-kms/aws-kms-mrk-match-for-decrypt.txt#2.5 + //# Otherwise if either input is not identified as a multi-Region key + //# (aws-kms-key-arn.md#identifying-an-aws-kms-multi-region-key), then + //# this function MUST return "false". + const arn1 = parseAwsKmsKeyArn(keyId1) + const arn2 = parseAwsKmsKeyArn(keyId2) + if (!arn1 || !arn2) return false + if (!isMultiRegionAwsKmsArn(arn1) || !isMultiRegionAwsKmsArn(arn2)) + return false + + //= compliance/framework/aws-kms/aws-kms-mrk-match-for-decrypt.txt#2.5 + //# Otherwise if both inputs are + //# identified as a multi-Region keys (aws-kms-key-arn.md#identifying-an- + //# aws-kms-multi-region-key), this function MUST return the result of + //# comparing the "partition", "service", "accountId", "resourceType", + //# and "resource" parts of both ARN inputs. + return ( + arn1.Partition === arn2.Partition && + arn1.AccountId === arn2.AccountId && + arn1.ResourceType === arn2.ResourceType && + arn1.ResourceId === arn2.ResourceId + ) +} + +/* Manually construct a new MRK ARN that looks like the old ARN except the region is replaced by a new region. + * Throws an error if the input parsed ARN is not an MRK + */ +export function constructArnInOtherRegion( + parsedArn: ParsedAwsKmsKeyArn, + region: string +): string { + /* Precondition: Only reconstruct a multi region ARN. */ + needs( + isMultiRegionAwsKmsArn(parsedArn), + 'Cannot attempt to construct an ARN in a new region from an non-MRK ARN.' + ) + const { Partition, AccountId, ResourceType, ResourceId } = parsedArn + return [ + ARN_PREFIX, + Partition, + KMS_SERVICE, + region, + AccountId, + ResourceType + '/' + ResourceId, + ].join(':') +} diff --git a/modules/kms-keyring/src/aws_kms_mrk_are_unique.ts b/modules/kms-keyring/src/aws_kms_mrk_are_unique.ts new file mode 100644 index 000000000..e9ad09557 --- /dev/null +++ b/modules/kms-keyring/src/aws_kms_mrk_are_unique.ts @@ -0,0 +1,44 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { isMultiRegionAwsKmsIdentifier, parseAwsKmsKeyArn } from './arn_parsing' + +//= compliance/framework/aws-kms/aws-kms-mrk-are-unique.txt#2.5 +//# The caller MUST provide: +export function awsKmsMrkAreUnique(awsKmsIdentifers: string[]): void { + const multiRegionKeys = awsKmsIdentifers.filter((i) => + isMultiRegionAwsKmsIdentifier(i) + ) + + //= compliance/framework/aws-kms/aws-kms-mrk-are-unique.txt#2.5 + //# If the list does not contain any multi-Region keys (aws-kms-key- + //# arn.md#identifying-an-aws-kms-multi-region-key) this function MUST + //# exit successfully. + if (!multiRegionKeys.length) return + + const multiRegionKeyIds = multiRegionKeys.map((mrk) => { + const arn = parseAwsKmsKeyArn(mrk) + return arn ? arn.ResourceId : mrk + }) + //= compliance/framework/aws-kms/aws-kms-mrk-are-unique.txt#2.5 + //# If there are zero duplicate resource ids between the multi-region + //# keys, this function MUST exit successfully + if (new Set(multiRegionKeyIds).size === multiRegionKeys.length) return + + //= compliance/framework/aws-kms/aws-kms-mrk-are-unique.txt#2.5 + //# If any duplicate multi-region resource ids exist, this function MUST + //# yield an error that includes all identifiers with duplicate resource + //# ids not only the first duplicate found. + const duplicateMultiRegionIdentifiers = multiRegionKeyIds + .map((mrk, i, a) => { + if (a.indexOf(mrk) !== a.lastIndexOf(mrk)) return multiRegionKeys[i] + /* Postcondition: Remove non-duplicate multi-Region keys. */ + return false + }) + .filter((dup) => dup) + .join(',') + + throw new Error( + `Related multi-Region keys: ${duplicateMultiRegionIdentifiers} are not allowed.` + ) +} diff --git a/modules/kms-keyring/src/helpers.ts b/modules/kms-keyring/src/helpers.ts index e4e69ba18..1e6fd5077 100644 --- a/modules/kms-keyring/src/helpers.ts +++ b/modules/kms-keyring/src/helpers.ts @@ -11,7 +11,7 @@ import { DecryptResponse, RequiredDecryptResponse, } from './kms_types' -import { regionFromKmsKeyArn } from './region_from_kms_key_arn' +import { getRegionFromIdentifier } from './arn_parsing' import { EncryptionContext, EncryptedDataKey, @@ -21,14 +21,16 @@ import { export const KMS_PROVIDER_ID = 'aws-kms' export async function generateDataKey( - clientProvider: KmsClientSupplier, + clientProvider: KmsClientSupplier | Client, NumberOfBytes: number, KeyId: string, EncryptionContext: EncryptionContext, GrantTokens?: string[] ): Promise { - const region = regionFromKmsKeyArn(KeyId) - const client = clientProvider(region) + const client = + typeof clientProvider === 'function' + ? clientProvider(getRegionFromIdentifier(KeyId)) + : clientProvider /* Check for early return (Postcondition): clientProvider did not return a client for generateDataKey. */ if (!client) return false @@ -46,14 +48,16 @@ export async function generateDataKey( } export async function encrypt( - clientProvider: KmsClientSupplier, + clientProvider: KmsClientSupplier | Client, Plaintext: Uint8Array, KeyId: string, EncryptionContext: EncryptionContext, GrantTokens?: string[] ): Promise { - const region = regionFromKmsKeyArn(KeyId) - const client = clientProvider(region) + const client = + typeof clientProvider === 'function' + ? clientProvider(getRegionFromIdentifier(KeyId)) + : clientProvider /* Check for early return (Postcondition): clientProvider did not return a client for encrypt. */ if (!client) return false @@ -72,15 +76,18 @@ export async function encrypt( } export async function decrypt( - clientProvider: KmsClientSupplier, + clientProvider: KmsClientSupplier | Client, { providerId, providerInfo, encryptedDataKey }: EncryptedDataKey, EncryptionContext: EncryptionContext, GrantTokens?: string[] ): Promise { /* Precondition: The EDK must be a KMS edk. */ needs(providerId === KMS_PROVIDER_ID, 'Unsupported providerId') - const region = regionFromKmsKeyArn(providerInfo) - const client = clientProvider(region) + const client = + typeof clientProvider === 'function' + ? clientProvider(getRegionFromIdentifier(providerInfo)) + : clientProvider + /* Check for early return (Postcondition): clientProvider did not return a client for decrypt. */ if (!client) return false diff --git a/modules/kms-keyring/src/index.ts b/modules/kms-keyring/src/index.ts index 8e3b01653..1e47aef75 100644 --- a/modules/kms-keyring/src/index.ts +++ b/modules/kms-keyring/src/index.ts @@ -3,5 +3,9 @@ export * from './kms_client_supplier' export * from './kms_keyring' +export * from './kms_mrk_keyring' +export * from './kms_mrk_discovery_keyring' export * from './helpers' export * from './region_from_kms_key_arn' +export * from './kms_mrk_strict_multi_keyring' +export * from './kms_mrk_discovery_multi_keyring' diff --git a/modules/kms-keyring/src/kms_keyring.ts b/modules/kms-keyring/src/kms_keyring.ts index 20a9bd22e..a9c7425b5 100644 --- a/modules/kms-keyring/src/kms_keyring.ts +++ b/modules/kms-keyring/src/kms_keyring.ts @@ -15,6 +15,7 @@ import { immutableClass, readOnlyProperty, unwrapDataKey, + Newable, } from '@aws-crypto/material-management' import { KMS_PROVIDER_ID, @@ -23,10 +24,7 @@ import { decrypt, kmsResponseToEncryptedDataKey, } from './helpers' -import { - regionFromKmsKeyArn, - decomposeAwsKmsKeyArn, -} from './region_from_kms_key_arn' +import { validAwsKmsIdentifier, parseAwsKmsKeyArn } from './arn_parsing' export interface KmsKeyringInput { clientProvider: KmsClientSupplier @@ -40,7 +38,7 @@ export interface KmsKeyringInput { } } -export interface KeyRing< +export interface KmsKeyRing< S extends SupportedAlgorithmSuites, Client extends AwsEsdkKMSInterface > extends Keyring { @@ -64,18 +62,14 @@ export interface KmsKeyRingConstructible< S extends SupportedAlgorithmSuites, Client extends AwsEsdkKMSInterface > { - new (input: KmsKeyringInput): KeyRing -} - -export interface KeyRingConstructible { - new (): Keyring + new (input: KmsKeyringInput): KmsKeyRing } export function KmsKeyringClass< S extends SupportedAlgorithmSuites, Client extends AwsEsdkKMSInterface ->(BaseKeyring: KeyRingConstructible): KmsKeyRingConstructible { - class KmsKeyring extends BaseKeyring implements KeyRing { +>(BaseKeyring: Newable>): KmsKeyRingConstructible { + class KmsKeyring extends BaseKeyring implements KmsKeyRing { public keyIds!: ReadonlyArray public generatorKeyId?: string public clientProvider!: KmsClientSupplier @@ -130,14 +124,11 @@ export function KmsKeyringClass< /* Precondition: All KMS key identifiers must be valid. */ needs( - !generatorKeyId || - typeof regionFromKmsKeyArn(generatorKeyId) === 'string', + !generatorKeyId || validAwsKmsIdentifier(generatorKeyId), 'Malformed arn.' ) needs( - keyIds.every( - (keyArn) => typeof regionFromKmsKeyArn(keyArn) === 'string' - ), + keyIds.every((keyArn) => validAwsKmsIdentifier(keyArn)), 'Malformed arn.' ) /* Precondition: clientProvider needs to be a callable function. */ @@ -277,7 +268,7 @@ export function KmsKeyringClass< /* Postcondition: The KeyId from KMS must match the encoded KeyID. */ needs( dataKey.KeyId === edk.providerInfo, - 'KMS Decryption key does not match serialized provider.' + 'KMS Decryption key does not match the requested key id.' ) const flags = @@ -324,7 +315,7 @@ export function KmsKeyringClass< function filterEDKs< S extends SupportedAlgorithmSuites, Client extends AwsEsdkKMSInterface ->(keyIds: string[], { isDiscovery, discoveryFilter }: KeyRing) { +>(keyIds: string[], { isDiscovery, discoveryFilter }: KmsKeyRing) { return function filter({ providerId, providerInfo }: EncryptedDataKey) { /* Check for early return (Postcondition): Only AWS KMS EDK should be attempted. */ if (providerId !== KMS_PROVIDER_ID) return false @@ -335,6 +326,9 @@ function filterEDKs< /* Check for early return (Postcondition): There is no discoveryFilter to further condition discovery. */ if (!discoveryFilter) return true + const parsedArn = parseAwsKmsKeyArn(providerInfo) + /* Postcondition: Provider info is a well formed AWS KMS ARN. */ + needs(parsedArn, 'Malformed arn in provider info.') /* If the providerInfo is an invalid ARN this will throw. * But, this function is also used to extract regions * from an CMK to generate a regional client. @@ -342,7 +336,7 @@ function filterEDKs< * that looks like a bare alias or key id. * However, these constructions will not have an account or partition. */ - const { account, partition } = decomposeAwsKmsKeyArn(providerInfo) + const { AccountId, Partition } = parsedArn /* Postcondition: The account and partition *must* match the discovery filter. * Since we are offering a runtime discovery of CMKs * it is best to have some form of filter on this. @@ -352,8 +346,8 @@ function filterEDKs< * when the AWS KMS Keyring is instantiated. */ return ( - discoveryFilter.partition === partition && - discoveryFilter.accountIDs.some((a) => a === account) + discoveryFilter.partition === Partition && + discoveryFilter.accountIDs.some((a) => a === AccountId) ) } else { /* Postcondition: The EDK CMK (providerInfo) *must* match a configured CMK. */ diff --git a/modules/kms-keyring/src/kms_mrk_discovery_keyring.ts b/modules/kms-keyring/src/kms_mrk_discovery_keyring.ts new file mode 100644 index 000000000..e915483d9 --- /dev/null +++ b/modules/kms-keyring/src/kms_mrk_discovery_keyring.ts @@ -0,0 +1,326 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + DecryptionMaterial, + EncryptedDataKey, + EncryptionMaterial, + immutableClass, + Keyring, + KeyringTrace, + KeyringTraceFlag, + needs, + readOnlyProperty, + SupportedAlgorithmSuites, + Newable, +} from '@aws-crypto/material-management' +import { + constructArnInOtherRegion, + isMultiRegionAwsKmsArn, + parseAwsKmsKeyArn, +} from './arn_parsing' +import { decrypt, KMS_PROVIDER_ID } from './helpers' +import { AwsEsdkKMSInterface, RequiredDecryptResponse } from './kms_types' + +export interface AwsKmsMrkAwareSymmetricDiscoveryKeyringInput< + Client extends AwsEsdkKMSInterface +> { + client: Client + discoveryFilter?: Readonly<{ + accountIDs: readonly string[] + partition: string + }> + grantTokens?: string[] +} + +export interface IAwsKmsMrkAwareSymmetricDiscoveryKeyring< + S extends SupportedAlgorithmSuites, + Client extends AwsEsdkKMSInterface +> extends Keyring { + client: Client + clientRegion: string + discoveryFilter?: Readonly<{ + accountIDs: readonly string[] + partition: string + }> + grantTokens?: string[] + _onEncrypt(material: EncryptionMaterial): Promise> + _onDecrypt( + material: DecryptionMaterial, + encryptedDataKeys: EncryptedDataKey[] + ): Promise> +} + +export interface AwsKmsMrkAwareSymmetricDiscoveryKeyringConstructible< + S extends SupportedAlgorithmSuites, + Client extends AwsEsdkKMSInterface +> { + new ( + input: AwsKmsMrkAwareSymmetricDiscoveryKeyringInput + ): IAwsKmsMrkAwareSymmetricDiscoveryKeyring +} + +export function AwsKmsMrkAwareSymmetricDiscoveryKeyringClass< + S extends SupportedAlgorithmSuites, + Client extends AwsEsdkKMSInterface +>( + BaseKeyring: Newable> +): AwsKmsMrkAwareSymmetricDiscoveryKeyringConstructible { + class AwsKmsMrkAwareSymmetricDiscoveryKeyring + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.5 + //# MUST implement that AWS Encryption SDK Keyring interface (../keyring- + //# interface.md#interface) + extends BaseKeyring + implements IAwsKmsMrkAwareSymmetricDiscoveryKeyring { + public client!: Client + public clientRegion!: string + public grantTokens?: string[] + public discoveryFilter?: Readonly<{ + accountIDs: readonly string[] + partition: string + }> + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.6 + //# On initialization the caller MUST provide: + constructor({ + client, + grantTokens, + discoveryFilter, + }: AwsKmsMrkAwareSymmetricDiscoveryKeyringInput) { + super() + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.6 + //# The keyring MUST know what Region the AWS KMS client is in. + // + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.6 + //# It + //# SHOULD obtain this information directly from the client as opposed to + //# having an additional parameter. + // + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.6 + //# However if it can not, then it MUST + //# NOT create the client itself. + // + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.6 + //# It SHOULD have a Region parameter and + //# SHOULD try to identify mismatched configurations. + // + // @ts-ignore the V3 client has set the config to protected + const clientRegion = client.config.region + needs(clientRegion, 'Client must be configured to a region.') + + /* Precondition: The AwsKmsMrkAwareSymmetricDiscoveryKeyring Discovery filter *must* be able to match something. + * I am not going to wait to tell you + * that no CMK can match an empty account list. + * e.g. [], [''], '' are not valid. + */ + needs( + !discoveryFilter || + (discoveryFilter.accountIDs && + discoveryFilter.accountIDs.length && + !!discoveryFilter.partition && + discoveryFilter.accountIDs.every( + (a) => typeof a === 'string' && !!a + )), + 'A discovery filter must be able to match something.' + ) + + readOnlyProperty(this, 'client', client) + readOnlyProperty(this, 'clientRegion', clientRegion) + readOnlyProperty(this, 'grantTokens', grantTokens) + readOnlyProperty( + this, + 'discoveryFilter', + discoveryFilter + ? Object.freeze({ + ...discoveryFilter, + accountIDs: Object.freeze(discoveryFilter.accountIDs), + }) + : discoveryFilter + ) + } + + async _onEncrypt(): Promise> { + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.7 + //# This function MUST fail. + throw new Error( + 'AwsKmsMrkAwareSymmetricDiscoveryKeyring cannot be used to encrypt' + ) + } + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 + //# OnDecrypt MUST take decryption materials (structures.md#decryption- + //# materials) and a list of encrypted data keys + //# (structures.md#encrypted-data-key) as input. + async _onDecrypt( + material: DecryptionMaterial, + encryptedDataKeys: EncryptedDataKey[] + ): Promise> { + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 + //# If the decryption materials (structures.md#decryption-materials) + //# already contained a valid plaintext data key OnDecrypt MUST + //# immediately return the unmodified decryption materials + //# (structures.md#decryption-materials). + if (material.hasValidKey()) return material + + const { client, grantTokens, clientRegion } = this + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 + //# The set of encrypted data keys MUST first be filtered to match this + //# keyring's configuration. + const decryptableEDKs = encryptedDataKeys.filter(filterEDKs(this)) + const cmkErrors: Error[] = [] + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 + //# For each encrypted data key in the filtered set, one at a time, the + //# OnDecrypt MUST attempt to decrypt the data key. + for (const edk of decryptableEDKs) { + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 + //# Otherwise it MUST + //# be the provider info. + let keyId = edk.providerInfo + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 + //# * "KeyId": If the provider info's resource type is "key" and its + //# resource is a multi-Region key then a new ARN MUST be created + //# where the region part MUST equal the AWS KMS client region and + //# every other part MUST equal the provider info. + const keyArn = parseAwsKmsKeyArn(edk.providerInfo) + needs(keyArn, 'Unexpected EDK ProviderInfo for AWS KMS EDK') + if (isMultiRegionAwsKmsArn(keyArn)) { + keyId = constructArnInOtherRegion(keyArn, clientRegion) + } + + let dataKey: RequiredDecryptResponse | false = false + try { + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 + //# When calling AWS KMS Decrypt + //# (https://docs.aws.amazon.com/kms/latest/APIReference/ + //# API_Decrypt.html), the keyring MUST call with a request constructed + //# as follows: + dataKey = await decrypt( + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 + //# To attempt to decrypt a particular encrypted data key + //# (structures.md#encrypted-data-key), OnDecrypt MUST call AWS KMS + //# Decrypt (https://docs.aws.amazon.com/kms/latest/APIReference/ + //# API_Decrypt.html) with the configured AWS KMS client. + client, + { + providerId: edk.providerId, + providerInfo: keyId, + encryptedDataKey: edk.encryptedDataKey, + }, + material.encryptionContext, + grantTokens + ) + /* This should be impossible given that decrypt only returns false if the client supplier does + * or if the providerId is not "aws-kms", which we have already filtered out + */ + if (!dataKey) continue + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 + //# * The "KeyId" field in the response MUST equal the requested "KeyId" + needs( + dataKey.KeyId === keyId, + 'KMS Decryption key does not match the requested key id.' + ) + + const flags = + KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY | + KeyringTraceFlag.WRAPPING_KEY_VERIFIED_ENC_CTX + const trace: KeyringTrace = { + keyNamespace: KMS_PROVIDER_ID, + keyName: dataKey.KeyId, + flags, + } + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 + //# * The length of the response's "Plaintext" MUST equal the key + //# derivation input length (algorithm-suites.md#key-derivation-input- + //# length) specified by the algorithm suite (algorithm-suites.md) + //# included in the input decryption materials + //# (structures.md#decryption-materials). + // + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 + //# Since the response does satisfies these requirements then OnDecrypt + //# MUST do the following with the response: + // + // setUnencryptedDataKey will throw if the plaintext does not match the algorithm suite requirements. + material.setUnencryptedDataKey(dataKey.Plaintext, trace) + return material + } catch (e) { + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 + //# If the response does not satisfies these requirements then an error + //# is collected and the next encrypted data key in the filtered set MUST + //# be attempted. + cmkErrors.push(e) + } + } + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 + //# If OnDecrypt fails to successfully decrypt any encrypted data key + //# (structures.md#encrypted-data-key), then it MUST yield an error that + //# includes all collected errors. + needs( + material.hasValidKey(), + [ + `Unable to decrypt data key${ + !decryptableEDKs.length ? ': No EDKs supplied' : '' + }.`, + ...cmkErrors.map((e, i) => `Error #${i + 1} \n${e.stack}`), + ].join('\n') + ) + return material + } + } + immutableClass(AwsKmsMrkAwareSymmetricDiscoveryKeyring) + return AwsKmsMrkAwareSymmetricDiscoveryKeyring +} + +function filterEDKs< + S extends SupportedAlgorithmSuites, + Client extends AwsEsdkKMSInterface +>({ + discoveryFilter, + clientRegion, +}: IAwsKmsMrkAwareSymmetricDiscoveryKeyring) { + return function filter({ providerId, providerInfo }: EncryptedDataKey) { + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 + //# * Its provider ID MUST exactly match the value "aws-kms". + if (providerId !== KMS_PROVIDER_ID) return false + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 + //# * The provider info MUST be a valid AWS KMS ARN (aws-kms-key- + //# arn.md#a-valid-aws-kms-arn) with a resource type of "key" or + //# OnDecrypt MUST fail. + const edkArn = parseAwsKmsKeyArn(providerInfo) + needs( + edkArn && edkArn.ResourceType === 'key', + 'Unexpected EDK ProviderInfo for AWS KMS EDK' + ) + const { + AccountId: account, + Partition: partition, + Region: edkRegion, + } = edkArn + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 + //# * If the provider info is not identified as a multi-Region key (aws- + //# kms-key-arn.md#identifying-an-aws-kms-multi-region-key), then the + //# provider info's Region MUST match the AWS KMS client region. + if (!isMultiRegionAwsKmsArn(edkArn) && clientRegion !== edkRegion) { + return false + } + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 + //# * If a discovery filter is configured, its partition and the + //# provider info partition MUST match. + // + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 + //# * If a discovery filter is configured, its set of accounts MUST + //# contain the provider info account. + return ( + !discoveryFilter || + (discoveryFilter.partition === partition && + discoveryFilter.accountIDs.includes(account)) + ) + } +} diff --git a/modules/kms-keyring/src/kms_mrk_discovery_multi_keyring.ts b/modules/kms-keyring/src/kms_mrk_discovery_multi_keyring.ts new file mode 100644 index 000000000..774e5300e --- /dev/null +++ b/modules/kms-keyring/src/kms_mrk_discovery_multi_keyring.ts @@ -0,0 +1,105 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + needs, + SupportedAlgorithmSuites, + MultiKeyring, + Newable, +} from '@aws-crypto/material-management' +import { IAwsKmsMrkAwareSymmetricDiscoveryKeyring, KmsClientSupplier } from '.' +import { AwsEsdkKMSInterface } from './kms_types' + +export interface AwsKmsMrkAwareDiscoveryMultiKeyringInput< + Client extends AwsEsdkKMSInterface +> { + regions: string[] + clientProvider?: KmsClientSupplier + discoveryFilter?: Readonly<{ + accountIDs: readonly string[] + partition: string + }> + grantTokens?: string[] +} + +export interface AwsKmsMrkAwareDiscoveryMultiKeyringBuilder< + S extends SupportedAlgorithmSuites, + Client extends AwsEsdkKMSInterface +> { + (input: AwsKmsMrkAwareDiscoveryMultiKeyringInput): MultiKeyring +} + +export function getAwsKmsMrkAwareDiscoveryMultiKeyringBuilder< + S extends SupportedAlgorithmSuites, + Client extends AwsEsdkKMSInterface +>( + MrkAwareDiscoveryKeyring: Newable< + IAwsKmsMrkAwareSymmetricDiscoveryKeyring + >, + MultiKeyring: Newable>, + defaultClientProvider: KmsClientSupplier +): AwsKmsMrkAwareDiscoveryMultiKeyringBuilder { + //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.5 + //# The caller MUST provide: + return function buildAwsKmsMrkAwareDiscoveryMultiKeyringNode({ + regions, + discoveryFilter, + //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.5 + //# If a regional client supplier is not passed, + //# then a default MUST be created that takes a region string and + //# generates a default AWS SDK client for the given region. + clientProvider = defaultClientProvider, + grantTokens, + }: AwsKmsMrkAwareDiscoveryMultiKeyringInput): MultiKeyring { + //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.5 + //# If an empty set of Region is provided this function MUST fail. + needs( + regions.length, + 'Configured regions must contain at least one region.' + ) + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.5 + //# If + //# any element of the set of regions is null or an empty string this + //# function MUST fail. + needs( + regions.every((region) => typeof region === 'string' && !!region), + 'Configured regions must not contain a null or empty string as a region.' + ) + + const children: IAwsKmsMrkAwareSymmetricDiscoveryKeyring< + S, + Client + >[] = regions + //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.5 + //# A set of AWS KMS clients MUST be created by calling regional client + //# supplier for each region in the input set of regions. + .map(clientProvider) + //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.5 + //# Then a set of AWS KMS MRK Aware Symmetric Region Discovery Keyring + //# (aws-kms-mrk-aware-symmetric-region-discovery-keyring.md) MUST be + //# created for each AWS KMS client by initializing each keyring with + .map((client) => { + /* Postcondition: If the configured clientProvider is not able to create a client for a defined region, throw an error. */ + needs( + client, + 'Configured clientProvider is unable to create a client for a configured region.' + ) + return new MrkAwareDiscoveryKeyring({ + client, + discoveryFilter, + grantTokens, + }) + }) + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.5 + //# Then a Multi-Keyring (../multi-keyring.md#inputs) MUST be initialize + //# by using this set of discovery keyrings as the child keyrings + //# (../multi-keyring.md#child-keyrings). + // + //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.5 + //# This Multi-Keyring MUST be + //# this functions output. + return new MultiKeyring({ children }) + } +} diff --git a/modules/kms-keyring/src/kms_mrk_keyring.ts b/modules/kms-keyring/src/kms_mrk_keyring.ts new file mode 100644 index 000000000..95a49586b --- /dev/null +++ b/modules/kms-keyring/src/kms_mrk_keyring.ts @@ -0,0 +1,393 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { AwsEsdkKMSInterface } from './kms_types' +import { + needs, + Keyring, + EncryptionMaterial, + DecryptionMaterial, + SupportedAlgorithmSuites, + KeyringTrace, + KeyringTraceFlag, + EncryptedDataKey, + immutableClass, + readOnlyProperty, + unwrapDataKey, + Newable, +} from '@aws-crypto/material-management' +import { + KMS_PROVIDER_ID, + generateDataKey, + encrypt, + decrypt, + kmsResponseToEncryptedDataKey, +} from './helpers' +import { + mrkAwareAwsKmsKeyIdCompare, + parseAwsKmsKeyArn, + validAwsKmsIdentifier, +} from './arn_parsing' + +export interface AwsKmsMrkAwareSymmetricKeyringInput< + Client extends AwsEsdkKMSInterface +> { + keyId: string + client: Client + grantTokens?: string[] +} + +export interface IAwsKmsMrkAwareSymmetricKeyring< + S extends SupportedAlgorithmSuites, + Client extends AwsEsdkKMSInterface +> extends Keyring { + keyId: string + client: Client + grantTokens?: string[] + _onEncrypt(material: EncryptionMaterial): Promise> + _onDecrypt( + material: DecryptionMaterial, + encryptedDataKeys: EncryptedDataKey[] + ): Promise> +} + +export interface AwsKmsMrkAwareSymmetricKeyringConstructible< + S extends SupportedAlgorithmSuites, + Client extends AwsEsdkKMSInterface +> { + new ( + input: AwsKmsMrkAwareSymmetricKeyringInput + ): IAwsKmsMrkAwareSymmetricKeyring +} + +export function AwsKmsMrkAwareSymmetricKeyringClass< + S extends SupportedAlgorithmSuites, + Client extends AwsEsdkKMSInterface +>( + BaseKeyring: Newable> +): AwsKmsMrkAwareSymmetricKeyringConstructible { + class AwsKmsMrkAwareSymmetricKeyring + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.5 + //# MUST implement the AWS Encryption SDK Keyring interface (../keyring- + //# interface.md#interface) + extends BaseKeyring + implements IAwsKmsMrkAwareSymmetricKeyring { + public keyId!: string + public client!: Client + public grantTokens?: string[] + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.6 + //# On initialization the caller MUST provide: + constructor({ + client, + keyId, + grantTokens, + }: AwsKmsMrkAwareSymmetricKeyringInput) { + super() + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.6 + //# The AWS KMS key identifier MUST NOT be null or empty. + needs( + keyId && typeof keyId === 'string', + 'An AWS KMS key identifier is required.' + ) + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.6 + //# The AWS KMS + //# key identifier MUST be a valid identifier (aws-kms-key-arn.md#a- + //# valid-aws-kms-identifier). + needs( + validAwsKmsIdentifier(keyId), + `Key id ${keyId} is not a valid identifier.` + ) + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.6 + //# The AWS KMS + //# SDK client MUST NOT be null. + needs(!!client, 'An AWS SDK client is required') + + readOnlyProperty(this, 'client', client) + readOnlyProperty(this, 'keyId', keyId) + readOnlyProperty(this, 'grantTokens', grantTokens) + } + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.7 + //# OnEncrypt MUST take encryption materials (structures.md#encryption- + //# materials) as input. + async _onEncrypt( + material: EncryptionMaterial + ): Promise> { + const { client, keyId, grantTokens } = this + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.7 + //# If the input encryption materials (structures.md#encryption- + //# materials) do not contain a plaintext data key OnEncrypt MUST attempt + //# to generate a new plaintext data key by calling AWS KMS + //# GenerateDataKey (https://docs.aws.amazon.com/kms/latest/APIReference/ + //# API_GenerateDataKey.html). + if (!material.hasUnencryptedDataKey) { + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.7 + //# The keyring MUST call + //# AWS KMS GenerateDataKeys with a request constructed as follows: + // + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.7 + //# If the call to AWS KMS GenerateDataKey + //# (https://docs.aws.amazon.com/kms/latest/APIReference/ + //# API_GenerateDataKey.html) does not succeed, OnEncrypt MUST NOT modify + //# the encryption materials (structures.md#encryption-materials) and + //# MUST fail. + const dataKey = await generateDataKey( + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.7 + //# If the keyring calls AWS KMS GenerateDataKeys, it MUST use the + //# configured AWS KMS client to make the call. + client, + material.suite.keyLengthBytes, + keyId, + material.encryptionContext, + grantTokens + ) + /* This should be impossible given that generateDataKey only returns false if the client supplier does. */ + needs(dataKey, 'Generator KMS key did not generate a data key') + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.7 + //# The Generate Data Key response's "KeyId" MUST be A valid AWS + //# KMS key ARN (aws-kms-key-arn.md#identifying-an-aws-kms-multi-region- + //# key). + needs(parseAwsKmsKeyArn(dataKey.KeyId), 'Malformed arn.') + + const flags = + KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY | + KeyringTraceFlag.WRAPPING_KEY_SIGNED_ENC_CTX | + KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY + const trace: KeyringTrace = { + keyNamespace: KMS_PROVIDER_ID, + keyName: dataKey.KeyId, + flags, + } + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.7 + //# If verified, + //# OnEncrypt MUST do the following with the response from AWS KMS + //# GenerateDataKey (https://docs.aws.amazon.com/kms/latest/APIReference/ + //# API_GenerateDataKey.html): + material + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.7 + //# If the Generate Data Key call succeeds, OnEncrypt MUST verify that + //# the response "Plaintext" length matches the specification of the + //# algorithm suite (algorithm-suites.md)'s Key Derivation Input Length + //# field. + // + // setUnencryptedDataKey will throw if the plaintext does not match the algorithm suite requirements. + .setUnencryptedDataKey(dataKey.Plaintext, trace) + .addEncryptedDataKey( + kmsResponseToEncryptedDataKey(dataKey), + KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY | + KeyringTraceFlag.WRAPPING_KEY_SIGNED_ENC_CTX + ) + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.7 + //# * OnEncrypt MUST output the modified encryption materials + //# (structures.md#encryption-materials) + return material + } else { + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.7 + //# Given a plaintext data key in the encryption materials + //# (structures.md#encryption-materials), OnEncrypt MUST attempt to + //# encrypt the plaintext data key using the configured AWS KMS key + //# identifier. + + const unencryptedDataKey = unwrapDataKey( + material.getUnencryptedDataKey() + ) + + const flags = + KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY | + KeyringTraceFlag.WRAPPING_KEY_SIGNED_ENC_CTX + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.7 + //# If the call to AWS KMS Encrypt + //# (https://docs.aws.amazon.com/kms/latest/APIReference/ + //# API_Encrypt.html) does not succeed, OnEncrypt MUST fail. + // + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.7 + //# The keyring + //# MUST AWS KMS Encrypt call with a request constructed as follows: + const kmsEDK = await encrypt( + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.7 + //# The keyring MUST call AWS KMS Encrypt + //# (https://docs.aws.amazon.com/kms/latest/APIReference/ + //# API_Encrypt.html) using the configured AWS KMS client. + client, + unencryptedDataKey, + keyId, + material.encryptionContext, + grantTokens + ) + + /* This should be impossible given that encrypt only returns false if the client supplier does. */ + needs( + kmsEDK, + 'AwsKmsMrkAwareSymmetricKeyring failed to encrypt data key' + ) + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.7 + //# If the Encrypt call succeeds The response's "KeyId" MUST be A valid + //# AWS KMS key ARN (aws-kms-key-arn.md#identifying-an-aws-kms-multi- + //# region-key). + needs(parseAwsKmsKeyArn(kmsEDK.KeyId), 'Malformed arn.') + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.7 + //# If verified, OnEncrypt MUST do the following with the response from + //# AWS KMS Encrypt (https://docs.aws.amazon.com/kms/latest/APIReference/ + //# API_Encrypt.html): + material.addEncryptedDataKey( + kmsResponseToEncryptedDataKey(kmsEDK), + flags + ) + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.7 + //# If all Encrypt calls succeed, OnEncrypt MUST output the modified + //# encryption materials (structures.md#encryption-materials). + return material + } + } + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.8 + //# OnDecrypt MUST take decryption materials (structures.md#decryption- + //# materials) and a list of encrypted data keys + //# (structures.md#encrypted-data-key) as input. + async _onDecrypt( + material: DecryptionMaterial, + encryptedDataKeys: EncryptedDataKey[] + ): Promise> { + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.8 + //# If the decryption materials (structures.md#decryption-materials) + //# already contained a valid plaintext data key OnDecrypt MUST + //# immediately return the unmodified decryption materials + //# (structures.md#decryption-materials). + if (material.hasValidKey()) return material + + const { client, keyId, grantTokens } = this + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.8 + //# The set of encrypted data keys MUST first be filtered to match this + //# keyring's configuration. + const decryptableEDKs = encryptedDataKeys.filter(filterEDKs(keyId)) + + const cmkErrors: Error[] = [] + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.8 + //# For each encrypted data key in the filtered set, one at a time, the + //# OnDecrypt MUST attempt to decrypt the data key. + for (const edk of decryptableEDKs) { + const { providerId, encryptedDataKey } = edk + try { + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.8 + //# When calling AWS KMS Decrypt + //# (https://docs.aws.amazon.com/kms/latest/APIReference/ + //# API_Decrypt.html), the keyring MUST call with a request constructed + //# as follows: + const dataKey = await decrypt( + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.8 + //# To attempt to decrypt a particular encrypted data key + //# (structures.md#encrypted-data-key), OnDecrypt MUST call AWS KMS + //# Decrypt (https://docs.aws.amazon.com/kms/latest/APIReference/ + //# API_Decrypt.html) with the configured AWS KMS client. + client, + // For MRKs the key identifier MUST be the configured key identifer. + { providerId, encryptedDataKey, providerInfo: this.keyId }, + material.encryptionContext, + grantTokens + ) + /* This should be impossible given that decrypt only returns false if the client supplier does + * or if the providerId is not "aws-kms", which we have already filtered out + */ + needs(dataKey, 'decrypt did not return a data key.') + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.8 + //# * The "KeyId" field in the response MUST equal the configured AWS + //# KMS key identifier. + needs( + dataKey.KeyId === this.keyId, + 'KMS Decryption key does not match the requested key id.' + ) + + const flags = + KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY | + KeyringTraceFlag.WRAPPING_KEY_VERIFIED_ENC_CTX + const trace: KeyringTrace = { + keyNamespace: KMS_PROVIDER_ID, + keyName: dataKey.KeyId, + flags, + } + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.8 + //# If the response does satisfies these requirements then OnDecrypt MUST + //# do the following with the response: + // + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.8 + //# * The length of the response's "Plaintext" MUST equal the key + //# derivation input length (algorithm-suites.md#key-derivation-input- + //# length) specified by the algorithm suite (algorithm-suites.md) + //# included in the input decryption materials + //# (structures.md#decryption-materials). + // + // setUnencryptedDataKey will throw if the plaintext does not match the algorithm suite requirements. + material.setUnencryptedDataKey(dataKey.Plaintext, trace) + return material + } catch (e) { + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.8 + //# If this attempt + //# results in an error, then these errors MUST be collected. + // + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.8 + //# If the response does not satisfies these requirements then an error + //# MUST be collected and the next encrypted data key in the filtered set + //# MUST be attempted. + cmkErrors.push(e) + } + } + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.8 + //# If OnDecrypt fails to successfully decrypt any encrypted data key + //# (structures.md#encrypted-data-key), then it MUST yield an error that + //# includes all the collected errors. + needs( + material.hasValidKey(), + [ + `Unable to decrypt data key${ + !decryptableEDKs.length ? ': No EDKs supplied' : '' + }.`, + ...cmkErrors.map((e, i) => `Error #${i + 1} \n${e.stack}`), + ].join('\n') + ) + + return material + } + } + immutableClass(AwsKmsMrkAwareSymmetricKeyring) + return AwsKmsMrkAwareSymmetricKeyring +} + +function filterEDKs(keyringKeyId: string) { + return function filter({ providerId, providerInfo }: EncryptedDataKey) { + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.8 + //# * Its provider ID MUST exactly match the value "aws-kms". + if (providerId !== KMS_PROVIDER_ID) return false + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.8 + //# * The provider info MUST be a valid AWS KMS ARN (aws-kms-key- + //# arn.md#a-valid-aws-kms-arn) with a resource type of "key" or + //# OnDecrypt MUST fail. + const arnInfo = parseAwsKmsKeyArn(providerInfo) + needs( + arnInfo && arnInfo.ResourceType === 'key', + 'Unexpected EDK ProviderInfo for AWS KMS EDK' + ) + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.8 + //# * The the function AWS KMS MRK Match for Decrypt (aws-kms-mrk-match- + //# for-decrypt.md#implementation) called with the configured AWS KMS + //# key identifier and the provider info MUST return "true". + return mrkAwareAwsKmsKeyIdCompare(keyringKeyId, providerInfo) + } +} diff --git a/modules/kms-keyring/src/kms_mrk_strict_multi_keyring.ts b/modules/kms-keyring/src/kms_mrk_strict_multi_keyring.ts new file mode 100644 index 000000000..aa6ad12e5 --- /dev/null +++ b/modules/kms-keyring/src/kms_mrk_strict_multi_keyring.ts @@ -0,0 +1,144 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + needs, + SupportedAlgorithmSuites, + MultiKeyring, + Newable, +} from '@aws-crypto/material-management' +import { IAwsKmsMrkAwareSymmetricKeyring, KmsClientSupplier } from '.' +import { AwsEsdkKMSInterface } from './kms_types' +import { getRegionFromIdentifier } from './arn_parsing' +import { awsKmsMrkAreUnique } from './aws_kms_mrk_are_unique' + +export interface AwsKmsMrkAwareStrictMultiKeyringInput< + Client extends AwsEsdkKMSInterface +> { + clientProvider?: KmsClientSupplier + generatorKeyId?: string + keyIds?: string[] + grantTokens?: string[] +} + +export interface AwsKmsMrkAwareStrictMultiKeyringBuilder< + S extends SupportedAlgorithmSuites, + Client extends AwsEsdkKMSInterface +> { + (input: AwsKmsMrkAwareStrictMultiKeyringInput): MultiKeyring +} + +export function getAwsKmsMrkAwareStrictMultiKeyringBuilder< + S extends SupportedAlgorithmSuites, + Client extends AwsEsdkKMSInterface +>( + MrkAwareKeyring: Newable>, + MultiKeyring: Newable>, + defaultClientProvider: KmsClientSupplier +): AwsKmsMrkAwareStrictMultiKeyringBuilder { + //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.6 + //# The caller MUST provide: + return function buildAwsKmsMrkAwareStrictMultiKeyring({ + generatorKeyId, + keyIds = [], + //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.6 + //# If + //# a regional client supplier is not passed, then a default MUST be + //# created that takes a region string and generates a default AWS SDK + //# client for the given region. + clientProvider = defaultClientProvider, + grantTokens, + }: AwsKmsMrkAwareStrictMultiKeyringInput = {}): MultiKeyring { + const identifier2Client = identifier2ClientBuilder(clientProvider) + + const allIdentifiers = generatorKeyId ? [generatorKeyId, ...keyIds] : keyIds + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.6 + //# At least one non-null or non-empty string AWS + //# KMS key identifiers exists in the input this function MUST fail. + // + //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.6 + //# If any of the AWS KMS key identifiers is null or an empty string this + //# function MUST fail. + needs( + allIdentifiers.length && + allIdentifiers.every((key) => typeof key === 'string' && key !== ''), + 'Noop keyring is not allowed: Set a generatorKeyId or at least one keyId.' + ) + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.6 + //# All + //# AWS KMS identifiers are passed to Assert AWS KMS MRK are unique (aws- + //# kms-mrk-are-unique.md#Implementation) and the function MUST return + //# success otherwise this MUST fail. + awsKmsMrkAreUnique(allIdentifiers) + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.6 + //# If there is a generator input then the generator keyring MUST be a + //# AWS KMS MRK Aware Symmetric Keyring (aws-kms-mrk-aware-symmetric- + //# keyring.md) initialized with + const generator = generatorKeyId + ? new MrkAwareKeyring({ + //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.6 + //# * The AWS KMS client that MUST be created by the regional client + //# supplier when called with the region part of the generator ARN or + //# a signal for the AWS SDK to select the default region. + client: identifier2Client(generatorKeyId), + keyId: generatorKeyId, + grantTokens, + }) + : undefined + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.6 + //# If there is a set of child identifiers then a set of AWS KMS MRK + //# Aware Symmetric Keyring (aws-kms-mrk-aware-symmetric-keyring.md) MUST + //# be created for each AWS KMS key identifier by initialized each + //# keyring with + const children: IAwsKmsMrkAwareSymmetricKeyring[] = keyIds.map( + (keyId) => { + return new MrkAwareKeyring({ + //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.6 + //# * The AWS KMS client that MUST be created by the regional client + //# supplier when called with the region part of the AWS KMS key + //# identifier or a signal for the AWS SDK to select the default + //# region. + client: identifier2Client(keyId), + keyId, + grantTokens, + }) + } + ) + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.6 + //# Then a Multi-Keyring (../multi-keyring.md#inputs) MUST be initialize + //# by using this generator keyring as the generator keyring (../multi- + //# keyring.md#generator-keyring) and this set of child keyrings as the + //# child keyrings (../multi-keyring.md#child-keyrings). + // + //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.6 + //# This Multi- + //# Keyring MUST be this functions output. + return new MultiKeyring({ + generator, + children, + }) + } +} + +export function identifier2ClientBuilder( + clientProvider: KmsClientSupplier +): (identifier: string) => Client { + return function identifier2Client(identifier: string): Client { + //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.6 + //# NOTE: The AWS Encryption SDK SHOULD NOT attempt to evaluate its own + //# default region. + const region = getRegionFromIdentifier(identifier) + const client = clientProvider(region) + /* Postcondition: If the configured clientProvider is not able to create a client for a defined generator key, throw an error. */ + needs( + client, + `Configured clientProvider is unable to create a client for configured ${identifier}.` + ) + return client + } +} diff --git a/modules/kms-keyring/src/region_from_kms_key_arn.ts b/modules/kms-keyring/src/region_from_kms_key_arn.ts index c0bcd8f34..8f4a3f5cb 100644 --- a/modules/kms-keyring/src/region_from_kms_key_arn.ts +++ b/modules/kms-keyring/src/region_from_kms_key_arn.ts @@ -13,14 +13,22 @@ import { needs } from '@aws-crypto/material-management' const aliasOrKeyResourceType = /^(alias|key)(\/.*)*$/ /* Maintaining function for backwards compatibility. */ +/** + * @deprecated Because decomposeAwsKmsKeyArn is incorrect, + * use parseAwsKmsIdentifier or parseAwsKmsKeyArn. + */ export function regionFromKmsKeyArn(kmsKeyArn: string): string { const { region } = decomposeAwsKmsKeyArn(kmsKeyArn) return region } +/** + * @deprecated This function incorrectly requires `key/12345678-1234-1234-1234-123456789012` + * AWS KMS requires that a raw key id be `12345678-1234-1234-1234-123456789012` + */ export function decomposeAwsKmsKeyArn( kmsKeyArn: string -): { region: string; account: string; partition: string } { +): { partition: string; region: string; account: string } { /* Precondition: A KMS key arn must be a string. */ needs(typeof kmsKeyArn === 'string', 'KMS key arn must be a string.') @@ -39,7 +47,7 @@ export function decomposeAwsKmsKeyArn( /* Postcondition: The ARN must be well formed. * The arn and kms section have defined values, * but the aws section does not. - * It is also possible to have a a key or alias. + * It is also possible to have a key or alias. * In this case the partition, service, region * will be empty. * In this case the arnLiteral should look like an alias. @@ -56,5 +64,5 @@ export function decomposeAwsKmsKeyArn( 'Malformed arn.' ) - return { region, account, partition } + return { partition, region, account } } diff --git a/modules/kms-keyring/test/arn_parsing.test.ts b/modules/kms-keyring/test/arn_parsing.test.ts new file mode 100644 index 000000000..117eb9809 --- /dev/null +++ b/modules/kms-keyring/test/arn_parsing.test.ts @@ -0,0 +1,471 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +/* eslint-env mocha */ + +import { expect } from 'chai' +import { + constructArnInOtherRegion, + isMultiRegionAwsKmsArn, + mrkAwareAwsKmsKeyIdCompare, + parseAwsKmsKeyArn, + parseAwsKmsResource, + validAwsKmsIdentifier, + isMultiRegionAwsKmsIdentifier, +} from '../src/arn_parsing' + +describe('parseAwsKmsKeyArn', () => { + it('parses a valid ARN', async () => { + const keyId = + 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012' + const parsedKeyArn = parseAwsKmsKeyArn(keyId) + expect(parsedKeyArn && parsedKeyArn.Partition).to.equal('aws') + expect(parsedKeyArn && parsedKeyArn.Region).to.equal('us-east-1') + expect(parsedKeyArn && parsedKeyArn.AccountId).to.equal('123456789012') + expect(parsedKeyArn && parsedKeyArn.ResourceType).to.equal('key') + expect(parsedKeyArn && parsedKeyArn.ResourceId).to.equal( + '12345678-1234-1234-1234-123456789012' + ) + }) + + it('parses a valid MRK ARN', async () => { + const keyId = + 'arn:aws:kms:us-east-1:123456789012:key/mrk-12345678123412341234123456789012' + const parsedKeyArn = parseAwsKmsKeyArn(keyId) + expect(parsedKeyArn && parsedKeyArn.Partition).to.equal('aws') + expect(parsedKeyArn && parsedKeyArn.Region).to.equal('us-east-1') + expect(parsedKeyArn && parsedKeyArn.AccountId).to.equal('123456789012') + expect(parsedKeyArn && parsedKeyArn.ResourceType).to.equal('key') + expect(parsedKeyArn && parsedKeyArn.ResourceId).to.equal( + 'mrk-12345678123412341234123456789012' + ) + }) + + it('parses a valid alias ARN', async () => { + const keyId = 'arn:aws:kms:us-east-1:123456789012:alias/example-alias' + const parsedKeyArn = parseAwsKmsKeyArn(keyId) + expect(parsedKeyArn && parsedKeyArn.Partition).to.equal('aws') + expect(parsedKeyArn && parsedKeyArn.Region).to.equal('us-east-1') + expect(parsedKeyArn && parsedKeyArn.AccountId).to.equal('123456789012') + expect(parsedKeyArn && parsedKeyArn.ResourceType).to.equal('alias') + expect(parsedKeyArn && parsedKeyArn.ResourceId).to.equal('example-alias') + }) + + it('Precondition: A KMS Key Id must be a non-null string.', async () => { + const bad = {} as any + expect(() => parseAwsKmsKeyArn(bad)).to.throw( + 'KMS key arn must be a non-null string.' + ) + expect(() => parseAwsKmsKeyArn('')).to.throw( + 'KMS key arn must be a non-null string.' + ) + }) + + it('Exceptional Postcondition: Only a valid AWS KMS resource.', () => { + expect(() => parseAwsKmsKeyArn('not/an/alias')).to.throw( + 'Malformed resource.' + ) + }) + + it('Check for early return (Postcondition): A valid ARN has 6 parts.', async () => { + expect(parseAwsKmsKeyArn('mrk-12345678123412341234123456789012')).to.equal( + false + ) + }) + + it('AWS KMS only accepts / as a resource delimiter.', async () => { + const keyId = 'alias:example-alias' + expect(() => parseAwsKmsKeyArn(keyId)).to.throw('Malformed arn.') + }) + + it('returns false on a valid alias with "/"', async () => { + const keyId = 'alias/example-alias' + expect(parseAwsKmsKeyArn(keyId)).to.equal(false) + }) + + it('throws an error for an invalid KeyId', async () => { + const keyId = 'Not:an/arn' + expect(() => parseAwsKmsKeyArn(keyId)).to.throw('Malformed arn.') + }) + + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 + //= type=test + //# MUST start with string "arn" + it('throws an error for an ARN that does not start with arn', async () => { + const keyId = + 'arn-not:aws:not-kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012' + expect(() => parseAwsKmsKeyArn(keyId)).to.throw('Malformed arn.') + }) + + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 + //= type=test + //# The partition MUST be a non-empty + it('throws an error for a missing partition ARN', async () => { + const keyId = + 'arn::kms::123456789012:key/12345678-1234-1234-1234-123456789012' + expect(() => parseAwsKmsKeyArn(keyId)).to.throw('Malformed arn.') + }) + + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 + //= type=test + //# The service MUST be the string "kms" + it('throws an error for an ARN without kms as service', async () => { + const keyId = + 'arn:aws:not-kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012' + expect(() => parseAwsKmsKeyArn(keyId)).to.throw('Malformed arn.') + }) + + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 + //= type=test + //# The region MUST be a non-empty string + it('throws an error for a missing region ARN', async () => { + const keyId = + 'arn:aws:kms::123456789012:key/12345678-1234-1234-1234-123456789012' + expect(() => parseAwsKmsKeyArn(keyId)).to.throw('Malformed arn.') + }) + + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 + //= type=test + //# The account MUST be a non-empty string + it('throws an error for a missing account ARN', async () => { + const keyId = + 'arn:aws:kms:us-east-1::key/12345678-1234-1234-1234-123456789012' + expect(() => parseAwsKmsKeyArn(keyId)).to.throw('Malformed arn.') + }) + + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 + //= type=test + //# The resource section MUST be non-empty and MUST be split by a + //# single "/" any additional "/" are included in the resource id + describe('Resource section', () => { + it('throws if the resource is missing', async () => { + const keyId = 'arn:aws:kms:us-east-1:123456789012:' + expect(() => parseAwsKmsKeyArn(keyId)).to.throw('Malformed arn.') + }) + + it('throws if the resource is delimited with ":".', async () => { + const keyId = + 'arn:aws:kms:us-east-1:123456789012:key:12345678-1234-1234-1234-123456789012' + expect(() => parseAwsKmsKeyArn(keyId)).to.throw('Malformed arn.') + }) + + it('an alias can contain slashes', async () => { + const keyId = + 'arn:aws:kms:us-east-1:123456789012:alias/with/extra/slashes' + const { ResourceId } = parseAwsKmsKeyArn(keyId) || {} + expect(ResourceId).to.equal('with/extra/slashes') + }) + + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 + //= type=test + //# The resource type MUST be either "alias" or "key" + it('throws if the resource type is not alias or key', async () => { + const keyId = + 'arn:aws:kms:us-east-1:123456789012:key-not/12345678-1234-1234-1234-123456789012' + expect(() => parseAwsKmsKeyArn(keyId)).to.throw('Malformed arn.') + }) + + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 + //= type=test + //# The resource id MUST be a non-empty string + it('throws if the resource id is missing', async () => { + const keyId = 'arn:aws:kms:us-east-1:123456789012:key' + expect(() => parseAwsKmsKeyArn(keyId)).to.throw('Malformed arn.') + }) + }) +}) + +describe('isMultiRegionAwsKmsArn', () => { + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.8 + //= type=test + //# This function MUST take a single AWS KMS ARN + // + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.8 + //= type=test + //# If resource type is "key" and resource ID starts with + //# "mrk-", this is a AWS KMS multi-Region key ARN and MUST return true. + it('returns true for an MRK ARN', async () => { + const key = + 'arn:aws:kms:us-west-2:123456789012:key/mrk-12345678123412341234123456789012' + expect(isMultiRegionAwsKmsArn(key)).to.equal(true) + // @ts-expect-error should be a valid arn + expect(isMultiRegionAwsKmsArn(parseAwsKmsKeyArn(key))).to.equal(true) + }) + + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.8 + //= type=test + //# If the input is an invalid AWS KMS ARN this function MUST error. + it('invalid arn will throw', () => { + expect(() => isMultiRegionAwsKmsArn('Not:an/arn')).to.throw( + 'Malformed arn.' + ) + }) + + it('Precondition: The kmsIdentifier must be an ARN.', () => { + expect(() => + isMultiRegionAwsKmsArn('mrk-12345678123412341234123456789012') + ).to.throw('AWS KMS identifier is not an ARN') + }) + + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.8 + //= type=test + //# If resource type is "alias", this is an AWS KMS alias ARN and MUST + //# return false. + it('returns false for an alias ARN with "mrk-"', async () => { + const key = + 'arn:aws:kms:us-west-2:123456789012:alias/mrk-12345678123412341234123456789012' + expect(isMultiRegionAwsKmsArn(key)).to.equal(false) + // @ts-expect-error should be a valid arn + expect(isMultiRegionAwsKmsArn(parseAwsKmsKeyArn(key))).to.equal(false) + }) + + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.8 + //= type=test + //# If resource type is "key" and resource ID does not start with "mrk-", + //# this is a (single-region) AWS KMS key ARN and MUST return false. + it('returns false for a non-MRK ARN', async () => { + const key = + 'arn:aws:kms:us-west-2:123456789012:key/12345678-1234-1234-1234-123456789012' + expect(isMultiRegionAwsKmsArn(key)).to.equal(false) + // @ts-expect-error should be a valid arn + expect(isMultiRegionAwsKmsArn(parseAwsKmsKeyArn(key))).to.equal(false) + }) +}) + +describe('isMultiRegionAwsKmsIdentifier', () => { + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.9 + //= type=test + //# This function MUST take a single AWS KMS identifier + it('can identify an ARN', () => { + const key = + 'arn:aws:kms:us-west-2:123456789012:key/mrk-12345678123412341234123456789012' + expect(isMultiRegionAwsKmsIdentifier(key)).to.equal(true) + }) + + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.9 + //= type=test + //# If the input starts with "arn:", this MUST return the output of + //# identifying an an AWS KMS multi-Region ARN (aws-kms-key- + //# arn.md#identifying-an-an-aws-kms-multi-region-arn) called with this + //# input. + it('can identify and arn', () => { + const key = + 'arn:aws:kms:us-west-2:123456789012:key/mrk-12345678123412341234123456789012' + expect(isMultiRegionAwsKmsIdentifier(key)).to.equal(true) + const arn = 'arn:aws:dynamodb:us-east-2:123456789012:table/myDynamoDBTable' + expect(() => isMultiRegionAwsKmsIdentifier(arn)).to.throw('Malformed arn.') + }) + + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.9 + //= type=test + //# If the input starts with "alias/", this an AWS KMS alias and + //# not a multi-Region key id and MUST return false. + it('is not confused by an alias', () => { + const alias = 'alias/mrk-12345678123412341234123456789012' + expect(isMultiRegionAwsKmsIdentifier(alias)).to.equal(false) + }) + + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.9 + //= type=test + //# If the input starts + //# with "mrk-", this is a multi-Region key id and MUST return true. + it('identifed a raw mulit region key resource', () => { + const keyId = 'mrk-12345678123412341234123456789012' + expect(isMultiRegionAwsKmsIdentifier(keyId)).to.equal(true) + }) + + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.9 + //= type=test + //# If + //# the input does not start with any of the above, this is not a multi- + //# Region key id and MUST return false. + it('is not confused by a single region key', () => { + const keyId = 'b3537ef1-d8dc-4780-9f5a-55776cbb2f7f' + expect(isMultiRegionAwsKmsIdentifier(keyId)).to.equal(false) + }) +}) + +describe('mrkAwareAwsKmsKeyIdCompare', () => { + //= compliance/framework/aws-kms/aws-kms-mrk-match-for-decrypt.txt#2.5 + //= type=test + //# The caller MUST provide: + // + //= compliance/framework/aws-kms/aws-kms-mrk-match-for-decrypt.txt#2.5 + //= type=test + //# If both identifiers are identical, this function MUST return "true". + it('returns true comparing two identical IDs', async () => { + const keyId = + 'arn:aws:kms:us-east-1:123456789012:key/mrk-12345678123412341234123456789012' + expect(mrkAwareAwsKmsKeyIdCompare(keyId, keyId)).to.equal(true) + const keyAlias = 'alias/someAlias' + expect(mrkAwareAwsKmsKeyIdCompare(keyAlias, keyAlias)).to.equal(true) + }) + + it('returns true comparing two MRK ARNs that are identical except region', async () => { + const usEast1Key = + 'arn:aws:kms:us-east-1:123456789012:key/mrk-12345678123412341234123456789012' + const usWest2Key = + 'arn:aws:kms:us-west-2:123456789012:key/mrk-12345678123412341234123456789012' + expect(mrkAwareAwsKmsKeyIdCompare(usEast1Key, usWest2Key)).to.equal(true) + }) + + //= compliance/framework/aws-kms/aws-kms-mrk-match-for-decrypt.txt#2.5 + //= type=test + //# Otherwise if either input is not identified as a multi-Region key + //# (aws-kms-key-arn.md#identifying-an-aws-kms-multi-region-key), then + //# this function MUST return "false". + it('returns false for different identifiers that are not multi-Region keys.', () => { + expect( + mrkAwareAwsKmsKeyIdCompare( + 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012', + 'arn:aws:kms:us-west-1:123456789012:key/12345678-1234-1234-1234-123456789012' + ) + ).to.equal(false) + }) + + //= compliance/framework/aws-kms/aws-kms-mrk-match-for-decrypt.txt#2.5 + //= type=test + //# Otherwise if both inputs are + //# identified as a multi-Region keys (aws-kms-key-arn.md#identifying-an- + //# aws-kms-multi-region-key), this function MUST return the result of + //# comparing the "partition", "service", "accountId", "resourceType", + //# and "resource" parts of both ARN inputs. + it('returns false comparing two MRK ARNs that differ in more than region', async () => { + const keyId = + 'arn:aws:kms:us-east-1:123456789012:key/mrk-12345678123412341234123456789012' + const otherAccountKey = + 'arn:aws:kms:us-east-1:000000000000:key/mrk-12345678123412341234123456789012' + const otherPartitionKey = + 'arn:not-aws:kms:us-east-1:123456789012:key/mrk-12345678123412341234123456789012' + const otherResource = + 'arn:aws:kms:us-east-1:123456789012:key/mrk-00000000-0000-0000-0000-000000000000' + expect(mrkAwareAwsKmsKeyIdCompare(keyId, otherAccountKey)).to.equal(false) + expect(mrkAwareAwsKmsKeyIdCompare(keyId, otherPartitionKey)).to.equal(false) + expect(mrkAwareAwsKmsKeyIdCompare(keyId, otherResource)).to.equal(false) + }) + + it('returns false comparing an alias with a key ARN', async () => { + const keyAlias = 'alias/SomeKeyAlias' + const keyId = + 'arn:aws:kms:us-east-1:123456789012:key/mrk-12345678123412341234123456789012' + const keyAliasArn = 'arn:aws:kms:us-east-1:123456789012:alias/SomeKeyAlias' + expect(mrkAwareAwsKmsKeyIdCompare(keyAlias, keyId)).to.equal(false) + expect(mrkAwareAwsKmsKeyIdCompare(keyAlias, keyAliasArn)).to.equal(false) + }) + + it('returns false comparing two distinct aliases', async () => { + const keyAlias = 'alias/SomeKeyAlias' + const otherKeyAlias = 'alias/SomeOtherKeyAlias' + expect(mrkAwareAwsKmsKeyIdCompare(keyAlias, otherKeyAlias)).to.equal(false) + }) + + it('throws an error comparing an invalid ID', async () => { + const keyId = + 'arn:aws:kms:us-east-1:123456789012:key/mrk-12345678123412341234123456789012' + const badId = 'Not:an/Arn' + expect(() => mrkAwareAwsKmsKeyIdCompare(keyId, badId)).to.throw() + }) +}) + +describe('parseAwsKmsResource', () => { + it('basic use', () => { + const info = parseAwsKmsResource('12345678-1234-1234-1234-123456789012') + expect(info.ResourceId).to.equal('12345678-1234-1234-1234-123456789012') + expect(info.ResourceType).to.equal('key') + }) + + it('works on an alias', () => { + const info = parseAwsKmsResource('alias/my-alias') + expect(info.ResourceId).to.equal('my-alias') + expect(info.ResourceType).to.equal('alias') + }) + + it('works on an alias', () => { + const info = parseAwsKmsResource('alias/my/alias/with/slashes') + expect(info.ResourceId).to.equal('my/alias/with/slashes') + expect(info.ResourceType).to.equal('alias') + }) + + it('Precondition: An AWS KMS resource can not have a `:`.', () => { + const keyId = + 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012' + expect(() => parseAwsKmsResource(keyId)).to.throw('Malformed resource.') + }) + + it('Precondition: A raw identifer is only an alias or a key.', () => { + expect(() => parseAwsKmsResource('anything/with a slash')).to.throw( + 'Malformed resource.' + ) + }) +}) + +describe('validAwsKmsIdentifier', () => { + it('is able to parse valid identifiers', () => { + expect( + !!validAwsKmsIdentifier( + 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012' + ) + ).to.equal(true) + expect( + !!validAwsKmsIdentifier( + 'arn:aws:kms:us-west-2:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7' + ) + ).to.equal(true) + expect( + !!validAwsKmsIdentifier( + 'arn:aws:kms:us-east-1:123456789012:alias/example-alias' + ) + ).to.equal(true) + expect(!!validAwsKmsIdentifier('alias/my/alias/with/slashes')).to.equal( + true + ) + expect( + !!validAwsKmsIdentifier('12345678-1234-1234-1234-123456789012') + ).to.equal(true) + expect( + !!validAwsKmsIdentifier('mrk-80bd8ecdcd4342aebd84b7dc9da498a7') + ).to.equal(true) + }) + + it('throws for invalid identifiers', () => { + expect(() => validAwsKmsIdentifier('Not:an/arn')).to.throw('Malformed arn') + expect(() => validAwsKmsIdentifier('alias:no')).to.throw('Malformed arn') + expect(() => + validAwsKmsIdentifier( + 'arn:aws:dynamodb:us-east-2:123456789012:table/myDynamoDBTable' + ) + ).to.throw('Malformed arn') + expect(() => validAwsKmsIdentifier('')).to.throw( + 'KMS key arn must be a non-null string.' + ) + }) +}) + +describe('constructArnInOtherRegion', () => { + it('returns new ARN with region replaced', async () => { + const parsedArn = { + Partition: 'aws', + Region: 'us-west-2', + AccountId: '123456789012', + ResourceType: 'key', + ResourceId: 'mrk-12345678123412341234123456789012', + } + const region = 'us-east-1' + const expectedArn = + 'arn:aws:kms:us-east-1:123456789012:key/mrk-12345678123412341234123456789012' + expect(constructArnInOtherRegion(parsedArn, region)).to.equal(expectedArn) + }) + it('Precondition: Only reconstruct a multi region ARN.', async () => { + const parsedArn = { + Partition: 'aws', + Region: 'us-west-2', + AccountId: '123456789012', + ResourceType: 'key', + ResourceId: '12345678-1234-1234-1234-123456789012', + } + const region = 'us-east-1' + expect(() => constructArnInOtherRegion(parsedArn, region)).to.throw( + 'Cannot attempt to construct an ARN in a new region from an non-MRK ARN.' + ) + }) +}) diff --git a/modules/kms-keyring/test/aws_kms_mrk_are_unique.test.ts b/modules/kms-keyring/test/aws_kms_mrk_are_unique.test.ts new file mode 100644 index 000000000..f4c86b611 --- /dev/null +++ b/modules/kms-keyring/test/aws_kms_mrk_are_unique.test.ts @@ -0,0 +1,154 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +/* eslint-env mocha */ + +import { expect } from 'chai' +import { awsKmsMrkAreUnique } from '../src/aws_kms_mrk_are_unique' + +describe('awsKmsMrkAreUnique', () => { + //= compliance/framework/aws-kms/aws-kms-mrk-are-unique.txt#2.5 + //= type=test + //# The caller MUST provide: + it('basic usage', () => { + expect(() => + awsKmsMrkAreUnique([ + 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012', + 'arn:aws:kms:us-east-1:123456789012:key/mrk-12345678123412341234123456789012', + ]) + ).to.not.throw() + }) + + //= compliance/framework/aws-kms/aws-kms-mrk-are-unique.txt#2.5 + //= type=test + //# If the list does not contain any multi-Region keys (aws-kms-key- + //# arn.md#identifying-an-aws-kms-multi-region-key) this function MUST + //# exit successfully. + it('only multi-Region keys are an error', () => { + expect(() => + awsKmsMrkAreUnique([ + 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012', + 'arn:aws:kms:us-west-2:658956600833:alias/EncryptDecrypt', + 'b3537ef1-d8dc-4780-9f5a-55776cbb2f7f', + 'alias/my-alias', + 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012', + 'arn:aws:kms:us-west-2:658956600833:alias/EncryptDecrypt', + 'b3537ef1-d8dc-4780-9f5a-55776cbb2f7f', + 'alias/my-alias', + ]) + ).to.not.throw() + }) + + //= compliance/framework/aws-kms/aws-kms-mrk-are-unique.txt#2.5 + //= type=test + //# If there are zero duplicate resource ids between the multi-region + //# keys, this function MUST exit successfully + it('multi-region keys that do not duplicate ', () => { + expect(() => + awsKmsMrkAreUnique([ + 'arn:aws:kms:us-east-1:123456789012:key/mrk-12345678123412341234123456789012', + 'arn:aws:kms:us-east-1:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7', + ]) + ).to.not.throw() + }) + + //= compliance/framework/aws-kms/aws-kms-mrk-are-unique.txt#2.5 + //= type=test + //# If any duplicate multi-region resource ids exist, this function MUST + //# yield an error that includes all identifiers with duplicate resource + //# ids not only the first duplicate found. + describe('related multi-region keys are not allowed.', () => { + it('related multi-Region keys error', () => { + const relatedKeys = [ + 'arn:aws:kms:us-east-1:123456789012:key/mrk-12345678123412341234123456789012', + 'arn:aws:kms:us-west-2:123456789012:key/mrk-12345678123412341234123456789012', + ] + expect(() => awsKmsMrkAreUnique(relatedKeys)).to.throw( + 'Related multi-Region keys:' + ) + }) + + it('error contains the related keys', () => { + const relatedKeys = [ + 'arn:aws:kms:us-east-1:123456789012:key/mrk-12345678123412341234123456789012', + 'arn:aws:kms:us-west-2:123456789012:key/mrk-12345678123412341234123456789012', + ] + const keys = [ + 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012', + 'arn:aws:kms:us-west-2:658956600833:alias/EncryptDecrypt', + 'b3537ef1-d8dc-4780-9f5a-55776cbb2f7f', + 'alias/my-alias', + 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012', + 'arn:aws:kms:us-west-2:658956600833:alias/EncryptDecrypt', + 'b3537ef1-d8dc-4780-9f5a-55776cbb2f7f', + 'alias/my-alias', + ] + expect(() => awsKmsMrkAreUnique(keys.concat(relatedKeys))).to.throw( + relatedKeys.join(',') + ) + }) + + it('error even for raw key id', () => { + const relatedKeys = [ + 'arn:aws:kms:us-east-1:123456789012:key/mrk-12345678123412341234123456789012', + 'mrk-12345678123412341234123456789012', + ] + const keys = [ + 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012', + 'arn:aws:kms:us-west-2:658956600833:alias/EncryptDecrypt', + 'b3537ef1-d8dc-4780-9f5a-55776cbb2f7f', + 'alias/my-alias', + 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012', + 'arn:aws:kms:us-west-2:658956600833:alias/EncryptDecrypt', + 'b3537ef1-d8dc-4780-9f5a-55776cbb2f7f', + 'alias/my-alias', + ] + expect(() => awsKmsMrkAreUnique(keys.concat(relatedKeys))).to.throw( + relatedKeys.join(',') + ) + }) + + it('Postcondition: Remove non-duplicate multi-Region keys. ', () => { + const relatedKeys = [ + 'arn:aws:kms:us-east-1:123456789012:key/mrk-12345678123412341234123456789012', + 'mrk-12345678123412341234123456789012', + ] + const keys = [ + 'arn:aws:kms:us-east-1:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7', + 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012', + 'arn:aws:kms:us-west-2:658956600833:alias/EncryptDecrypt', + 'b3537ef1-d8dc-4780-9f5a-55776cbb2f7f', + 'alias/my-alias', + 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012', + 'arn:aws:kms:us-west-2:658956600833:alias/EncryptDecrypt', + 'b3537ef1-d8dc-4780-9f5a-55776cbb2f7f', + 'alias/my-alias', + ] + expect(() => awsKmsMrkAreUnique(keys.concat(relatedKeys))).to.throw( + relatedKeys.join(',') + ) + }) + + it('show all duplicates', () => { + const relatedKeys = [ + 'arn:aws:kms:us-east-1:123456789012:key/mrk-12345678123412341234123456789012', + 'mrk-12345678123412341234123456789012', + 'arn:aws:kms:us-east-1:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7', + 'arn:aws:kms:us-east-1:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7', + ] + const keys = [ + 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012', + 'arn:aws:kms:us-west-2:658956600833:alias/EncryptDecrypt', + 'b3537ef1-d8dc-4780-9f5a-55776cbb2f7f', + 'alias/my-alias', + 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012', + 'arn:aws:kms:us-west-2:658956600833:alias/EncryptDecrypt', + 'b3537ef1-d8dc-4780-9f5a-55776cbb2f7f', + 'alias/my-alias', + ] + expect(() => awsKmsMrkAreUnique(keys.concat(relatedKeys))).to.throw( + relatedKeys.join(',') + ) + }) + }) +}) diff --git a/modules/kms-keyring/test/kms_keyring.constructor.test.ts b/modules/kms-keyring/test/kms_keyring.constructor.test.ts index 8d0279ed5..6f9168189 100644 --- a/modules/kms-keyring/test/kms_keyring.constructor.test.ts +++ b/modules/kms-keyring/test/kms_keyring.constructor.test.ts @@ -4,8 +4,12 @@ /* eslint-env mocha */ import { expect } from 'chai' -import { KmsKeyringClass, KeyRingConstructible } from '../src/kms_keyring' -import { NodeAlgorithmSuite, Keyring } from '@aws-crypto/material-management' +import { KmsKeyringClass } from '../src/kms_keyring' +import { + NodeAlgorithmSuite, + Keyring, + Newable, +} from '@aws-crypto/material-management' describe('KmsKeyring: constructor', () => { it('set properties', () => { @@ -16,7 +20,7 @@ describe('KmsKeyring: constructor', () => { const grantTokens = ['grant'] class TestKmsKeyring extends KmsKeyringClass( - Keyring as KeyRingConstructible + Keyring as Newable> ) {} const test = new TestKmsKeyring({ @@ -38,7 +42,7 @@ describe('KmsKeyring: constructor', () => { const discoveryFilter = { accountIDs: ['123456789012'], partition: 'aws' } class TestKmsKeyring extends KmsKeyringClass( - Keyring as KeyRingConstructible + Keyring as Newable> ) {} const test = new TestKmsKeyring({ @@ -57,7 +61,7 @@ describe('KmsKeyring: constructor', () => { it('Precondition: This is an abstract class. (But TypeScript does not have a clean way to model this)', () => { const clientProvider: any = () => {} const KmsKeyring = KmsKeyringClass( - Keyring as KeyRingConstructible + Keyring as Newable> ) expect(() => new KmsKeyring({ clientProvider })).to.throw( 'new KmsKeyring is not allowed' @@ -66,7 +70,7 @@ describe('KmsKeyring: constructor', () => { it('Precondition: A noop KmsKeyring is not allowed.', () => { class TestKmsKeyring extends KmsKeyringClass( - Keyring as KeyRingConstructible + Keyring as Newable> ) {} const clientProvider: any = () => {} expect(() => new TestKmsKeyring({ clientProvider })).to.throw() @@ -74,7 +78,7 @@ describe('KmsKeyring: constructor', () => { it('Precondition: A keyring can be either a Discovery or have keyIds configured.', () => { class TestKmsKeyring extends KmsKeyringClass( - Keyring as KeyRingConstructible + Keyring as Newable> ) {} const clientProvider: any = () => {} const generatorKeyId = @@ -124,7 +128,7 @@ describe('KmsKeyring: constructor', () => { const clientProvider: any = () => {} class TestKmsKeyring extends KmsKeyringClass( - Keyring as KeyRingConstructible + Keyring as Newable> ) {} expect( () => @@ -154,7 +158,7 @@ describe('KmsKeyring: constructor', () => { const discovery = true class TestKmsKeyring extends KmsKeyringClass( - Keyring as KeyRingConstructible + Keyring as Newable> ) {} expect( () => @@ -170,14 +174,14 @@ describe('KmsKeyring: constructor', () => { it('Precondition: All KMS key identifiers must be valid.', () => { const clientProvider: any = () => {} class TestKmsKeyring extends KmsKeyringClass( - Keyring as KeyRingConstructible + Keyring as Newable> ) {} expect( () => new TestKmsKeyring({ clientProvider, - generatorKeyId: 'Not arn', + generatorKeyId: 'Not:an/arn', }) ).to.throw() @@ -185,7 +189,7 @@ describe('KmsKeyring: constructor', () => { () => new TestKmsKeyring({ clientProvider, - keyIds: ['Not arn'], + keyIds: ['Not:an/arn'], }) ).to.throw() @@ -195,7 +199,7 @@ describe('KmsKeyring: constructor', () => { clientProvider, keyIds: [ 'arn:aws:kms:us-east-1:123456789012:alias/example-alias', - 'Not arn', + 'Not:an/arn', ], }) ).to.throw() @@ -220,20 +224,20 @@ describe('KmsKeyring: constructor', () => { it('An KMS CMK alias is a valid CMK identifier', () => { const clientProvider: any = () => {} class TestKmsKeyring extends KmsKeyringClass( - Keyring as KeyRingConstructible + Keyring as Newable> ) {} const test = new TestKmsKeyring({ clientProvider, generatorKeyId: 'alias/example-alias', - keyIds: ['alias:example-alias'], + keyIds: ['alias/example-alias'], }) expect(test).to.be.instanceOf(TestKmsKeyring) }) it('Precondition: clientProvider needs to be a callable function.', () => { class TestKmsKeyring extends KmsKeyringClass( - Keyring as KeyRingConstructible + Keyring as Newable> ) {} const clientProvider: any = 'not function' const discovery = true diff --git a/modules/kms-keyring/test/kms_keyring.edk-order.test.ts b/modules/kms-keyring/test/kms_keyring.edk-order.test.ts index e9c530f53..d5e974560 100644 --- a/modules/kms-keyring/test/kms_keyring.edk-order.test.ts +++ b/modules/kms-keyring/test/kms_keyring.edk-order.test.ts @@ -5,7 +5,7 @@ import chai from 'chai' import chaiAsPromised from 'chai-as-promised' -import { KmsKeyringClass, KeyRingConstructible } from '../src/kms_keyring' +import { KmsKeyringClass } from '../src/kms_keyring' import { NodeAlgorithmSuite, AlgorithmSuiteIdentifier, @@ -13,6 +13,7 @@ import { EncryptedDataKey, Keyring, needs, + Newable, } from '@aws-crypto/material-management' chai.use(chaiAsPromised) const { expect } = chai @@ -29,7 +30,7 @@ describe('KmsKeyring: decrypt EDK order', () => { const edks = [...Array(5)].map(() => edk) const state = buildProviderState(edks, suite) class TestKmsKeyring extends KmsKeyringClass( - Keyring as KeyRingConstructible + Keyring as Newable> ) {} const testKeyring = new TestKmsKeyring({ @@ -57,7 +58,7 @@ describe('KmsKeyring: decrypt EDK order', () => { const state = buildProviderState(edks, suite, { failureCount: 1 }) class TestKmsKeyring extends KmsKeyringClass( - Keyring as KeyRingConstructible + Keyring as Newable> ) {} const testKeyring = new TestKmsKeyring({ @@ -83,7 +84,7 @@ describe('KmsKeyring: decrypt EDK order', () => { const edks = [...Array(5)].map(edkHelper).map(({ edk }) => edk) const state = buildProviderState(edks, suite, { edkSuccessIndex: 4 }) class TestKmsKeyring extends KmsKeyringClass( - Keyring as KeyRingConstructible + Keyring as Newable> ) {} const testKeyring = new TestKmsKeyring({ @@ -119,7 +120,7 @@ describe('KmsKeyring: decrypt EDK order', () => { calls += 1 } class TestKmsKeyring extends KmsKeyringClass( - Keyring as KeyRingConstructible + Keyring as Newable> ) {} const testKeyring = new TestKmsKeyring({ @@ -145,7 +146,7 @@ describe('KmsKeyring: decrypt EDK order', () => { const edks = [...Array(5)].map(edkHelper).map(({ edk }) => edk) const state = buildProviderState(edks, suite) class TestKmsKeyring extends KmsKeyringClass( - Keyring as KeyRingConstructible + Keyring as Newable> ) {} const testKeyring = new TestKmsKeyring({ @@ -172,7 +173,7 @@ describe('KmsKeyring: decrypt EDK order', () => { const state = buildProviderState(edks, suite, { failureCount: 1 }) class TestKmsKeyring extends KmsKeyringClass( - Keyring as KeyRingConstructible + Keyring as Newable> ) {} const testKeyring = new TestKmsKeyring({ @@ -204,7 +205,7 @@ describe('KmsKeyring: decrypt EDK order', () => { const state = buildProviderState(edks, suite, { edkSuccessIndex: 4 }) class TestKmsKeyring extends KmsKeyringClass( - Keyring as KeyRingConstructible + Keyring as Newable> ) {} const testKeyring = new TestKmsKeyring({ @@ -243,7 +244,7 @@ describe('KmsKeyring: decrypt EDK order', () => { } class TestKmsKeyring extends KmsKeyringClass( - Keyring as KeyRingConstructible + Keyring as Newable> ) {} const testKeyring = new TestKmsKeyring({ @@ -279,7 +280,7 @@ describe('KmsKeyring: decrypt EDK order', () => { } class TestKmsKeyring extends KmsKeyringClass( - Keyring as KeyRingConstructible + Keyring as Newable> ) {} const testKeyring = new TestKmsKeyring({ @@ -316,7 +317,7 @@ describe('KmsKeyring: decrypt EDK order', () => { } class TestKmsKeyring extends KmsKeyringClass( - Keyring as KeyRingConstructible + Keyring as Newable> ) {} const testKeyring = new TestKmsKeyring({ @@ -359,7 +360,7 @@ describe('KmsKeyring: decrypt EDK order', () => { } class TestKmsKeyring extends KmsKeyringClass( - Keyring as KeyRingConstructible + Keyring as Newable> ) {} const testKeyring = new TestKmsKeyring({ @@ -400,7 +401,7 @@ describe('KmsKeyring: decrypt EDK order', () => { } class TestKmsKeyring extends KmsKeyringClass( - Keyring as KeyRingConstructible + Keyring as Newable> ) {} const testKeyring = new TestKmsKeyring({ @@ -441,7 +442,7 @@ describe('KmsKeyring: decrypt EDK order', () => { } class TestKmsKeyring extends KmsKeyringClass( - Keyring as KeyRingConstructible + Keyring as Newable> ) {} const testKeyring = new TestKmsKeyring({ @@ -489,7 +490,7 @@ describe('KmsKeyring: decrypt EDK order', () => { failureCount: matchInfo.length, }) class TestKmsKeyring extends KmsKeyringClass( - Keyring as KeyRingConstructible + Keyring as Newable> ) {} const testKeyring = new TestKmsKeyring({ @@ -516,7 +517,7 @@ describe('KmsKeyring: decrypt EDK order', () => { edkSuccessIndex: 4, }) class TestKmsKeyring extends KmsKeyringClass( - Keyring as KeyRingConstructible + Keyring as Newable> ) {} const testKeyring = new TestKmsKeyring({ diff --git a/modules/kms-keyring/test/kms_keyring.ondecrypt.test.ts b/modules/kms-keyring/test/kms_keyring.ondecrypt.test.ts index 513cd397b..bf514c60f 100644 --- a/modules/kms-keyring/test/kms_keyring.ondecrypt.test.ts +++ b/modules/kms-keyring/test/kms_keyring.ondecrypt.test.ts @@ -5,7 +5,7 @@ import chai from 'chai' import chaiAsPromised from 'chai-as-promised' -import { KmsKeyringClass, KeyRingConstructible } from '../src/kms_keyring' +import { KmsKeyringClass } from '../src/kms_keyring' import { NodeAlgorithmSuite, AlgorithmSuiteIdentifier, @@ -13,6 +13,7 @@ import { NodeDecryptionMaterial, EncryptedDataKey, Keyring, + Newable, } from '@aws-crypto/material-management' chai.use(chaiAsPromised) const { expect } = chai @@ -48,7 +49,7 @@ describe('KmsKeyring: _onDecrypt', () => { } } class TestKmsKeyring extends KmsKeyringClass( - Keyring as KeyRingConstructible + Keyring as Newable> ) {} const testKeyring = new TestKmsKeyring({ @@ -111,7 +112,7 @@ describe('KmsKeyring: _onDecrypt', () => { } } class TestKmsKeyring extends KmsKeyringClass( - Keyring as KeyRingConstructible + Keyring as Newable> ) {} const testKeyring = new TestKmsKeyring({ @@ -145,6 +146,40 @@ describe('KmsKeyring: _onDecrypt', () => { ).to.equal(KeyringTraceFlag.WRAPPING_KEY_VERIFIED_ENC_CTX) }) + it('Postcondition: Provider info is a well formed AWS KMS ARN.', async () => { + const aliasArn = 'alias/example-alias' + const context = { some: 'context' } + const grantTokens = ['grant'] + const discovery = true + const suite = new NodeAlgorithmSuite( + AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16 + ) + + let kmsCalled = false + const clientProvider: any = () => { + kmsCalled = true + } + class TestKmsKeyring extends KmsKeyringClass( + Keyring as Newable> + ) {} + + const edk = new EncryptedDataKey({ + providerId: 'aws-kms', + providerInfo: aliasArn, + encryptedDataKey: Buffer.from(aliasArn), + }) + + await expect( + new TestKmsKeyring({ + clientProvider, + grantTokens, + discovery, + discoveryFilter: { partition: 'aws', accountIDs: ['123456789012'] }, + }).onDecrypt(new NodeDecryptionMaterial(suite, context), [edk]) + ).to.eventually.rejectedWith('Malformed arn in provider info.') + expect(kmsCalled).to.equal(false) + }) + it('decrypt errors should not halt', async () => { const generatorKeyId = 'arn:aws:kms:us-east-1:123456789012:alias/example-alias' @@ -182,7 +217,7 @@ describe('KmsKeyring: _onDecrypt', () => { } } class TestKmsKeyring extends KmsKeyringClass( - Keyring as KeyRingConstructible + Keyring as Newable> ) {} const testKeyring = new TestKmsKeyring({ @@ -227,7 +262,7 @@ describe('KmsKeyring: _onDecrypt', () => { kmsCalled = true } class TestKmsKeyring extends KmsKeyringClass( - Keyring as KeyRingConstructible + Keyring as Newable> ) {} const edk = new EncryptedDataKey({ @@ -261,7 +296,7 @@ describe('KmsKeyring: _onDecrypt', () => { kmsCalled = true } class TestKmsKeyring extends KmsKeyringClass( - Keyring as KeyRingConstructible + Keyring as Newable> ) {} const edk = new EncryptedDataKey({ @@ -314,7 +349,7 @@ describe('KmsKeyring: _onDecrypt', () => { const clientProvider: any = () => false class TestKmsKeyring extends KmsKeyringClass( - Keyring as KeyRingConstructible + Keyring as Newable> ) {} const testKeyring = new TestKmsKeyring({ @@ -363,7 +398,7 @@ describe('KmsKeyring: _onDecrypt', () => { } } class TestKmsKeyring extends KmsKeyringClass( - Keyring as KeyRingConstructible + Keyring as Newable> ) {} const testKeyring = new TestKmsKeyring({ @@ -386,7 +421,7 @@ describe('KmsKeyring: _onDecrypt', () => { ) ).to.rejectedWith( Error, - 'KMS Decryption key does not match serialized provider.' + 'KMS Decryption key does not match the requested key id.' ) }) @@ -418,7 +453,7 @@ describe('KmsKeyring: _onDecrypt', () => { } } class TestKmsKeyring extends KmsKeyringClass( - Keyring as KeyRingConstructible + Keyring as Newable> ) {} const testKeyring = new TestKmsKeyring({ @@ -462,7 +497,7 @@ describe('KmsKeyring: _onDecrypt', () => { } } class TestKmsKeyring extends KmsKeyringClass( - Keyring as KeyRingConstructible + Keyring as Newable> ) {} const testKeyring = new TestKmsKeyring({ @@ -514,7 +549,7 @@ describe('KmsKeyring: _onDecrypt', () => { kmsCalled = true } class TestKmsKeyring extends KmsKeyringClass( - Keyring as KeyRingConstructible + Keyring as Newable> ) {} const edk = new EncryptedDataKey({ diff --git a/modules/kms-keyring/test/kms_keyring.onencrypt.test.ts b/modules/kms-keyring/test/kms_keyring.onencrypt.test.ts index c90543370..ca73487ea 100644 --- a/modules/kms-keyring/test/kms_keyring.onencrypt.test.ts +++ b/modules/kms-keyring/test/kms_keyring.onencrypt.test.ts @@ -5,13 +5,14 @@ import chai from 'chai' import chaiAsPromised from 'chai-as-promised' -import { KmsKeyringClass, KeyRingConstructible } from '../src/kms_keyring' +import { KmsKeyringClass } from '../src/kms_keyring' import { NodeAlgorithmSuite, AlgorithmSuiteIdentifier, NodeEncryptionMaterial, KeyringTraceFlag, Keyring, + Newable, } from '@aws-crypto/material-management' chai.use(chaiAsPromised) const { expect } = chai @@ -50,7 +51,7 @@ describe('KmsKeyring: _onEncrypt', () => { } } class TestKmsKeyring extends KmsKeyringClass( - Keyring as KeyRingConstructible + Keyring as Newable> ) {} const testKeyring = new TestKmsKeyring({ @@ -116,7 +117,7 @@ describe('KmsKeyring: _onEncrypt', () => { return false } class TestKmsKeyring extends KmsKeyringClass( - Keyring as KeyRingConstructible + Keyring as Newable> ) {} const testKeyring = new TestKmsKeyring({ @@ -149,7 +150,7 @@ describe('KmsKeyring: _onEncrypt', () => { return false } class TestKmsKeyring extends KmsKeyringClass( - Keyring as KeyRingConstructible + Keyring as Newable> ) {} const testKeyring = new TestKmsKeyring({ @@ -186,7 +187,7 @@ describe('KmsKeyring: _onEncrypt', () => { } } class TestKmsKeyring extends KmsKeyringClass( - Keyring as KeyRingConstructible + Keyring as Newable> ) {} const testKeyring = new TestKmsKeyring({ @@ -219,7 +220,7 @@ describe('KmsKeyring: _onEncrypt', () => { return false } class TestKmsKeyring extends KmsKeyringClass( - Keyring as KeyRingConstructible + Keyring as Newable> ) {} const testKeyring = new TestKmsKeyring({ @@ -250,7 +251,7 @@ describe('KmsKeyring: _onEncrypt', () => { } } class TestKmsKeyring extends KmsKeyringClass( - Keyring as KeyRingConstructible + Keyring as Newable> ) {} const testKeyring = new TestKmsKeyring({ @@ -298,7 +299,7 @@ describe('KmsKeyring: _onEncrypt', () => { return false } class TestKmsKeyring extends KmsKeyringClass( - Keyring as KeyRingConstructible + Keyring as Newable> ) {} const testKeyring = new TestKmsKeyring({ diff --git a/modules/kms-keyring/test/kms_mrk_discovery_keyring.constructor.test.ts b/modules/kms-keyring/test/kms_mrk_discovery_keyring.constructor.test.ts new file mode 100644 index 000000000..03182bd98 --- /dev/null +++ b/modules/kms-keyring/test/kms_mrk_discovery_keyring.constructor.test.ts @@ -0,0 +1,104 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +/* eslint-env mocha */ + +import { expect } from 'chai' +import { AwsKmsMrkAwareSymmetricDiscoveryKeyringClass } from '../src/kms_mrk_discovery_keyring' +import { + NodeAlgorithmSuite, + Keyring, + Newable, +} from '@aws-crypto/material-management' + +describe('AwsKmsMrkAwareSymmetricDiscoveryKeyring: constructor', () => { + it('set properties', () => { + const client: any = { config: { region: 'us-west-2' } } + const grantTokens = ['grant'] + const discoveryFilter = { accountIDs: ['123456789012'], partition: 'aws' } + + class TestAwsKmsMrkAwareSymmetricDiscoveryKeyring extends AwsKmsMrkAwareSymmetricDiscoveryKeyringClass( + Keyring as Newable> + ) {} + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.6 + //= type=test + //# On initialization the caller MUST provide: + const test = new TestAwsKmsMrkAwareSymmetricDiscoveryKeyring({ + client, + discoveryFilter, + grantTokens, + }) + expect(test.client).to.equal(client) + expect(test.grantTokens).to.equal(grantTokens) + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.5 + //= type=test + //# MUST implement that AWS Encryption SDK Keyring interface (../keyring- + //# interface.md#interface) + expect((test as Keyring) instanceof Keyring).to.equal( + true + ) + }) + + it('The keyring MUST know what Region the AWS KMS client is in', () => { + const client: any = { config: {} } + const discoveryFilter = { accountIDs: ['123456789012'], partition: 'aws' } + + class TestAwsKmsMrkAwareSymmetricDiscoveryKeyring extends AwsKmsMrkAwareSymmetricDiscoveryKeyringClass( + Keyring as Newable> + ) {} + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.6 + //= type=test + //# The keyring MUST know what Region the AWS KMS client is in. + // + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.6 + //= type=test + //# It + //# SHOULD obtain this information directly from the client as opposed to + //# having an additional parameter. + // + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.6 + //= type=test + //# However if it can not, then it MUST + //# NOT create the client itself. + // + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.6 + //= type=test + //# It SHOULD have a Region parameter and + //# SHOULD try to identify mismatched configurations. + expect( + () => + new TestAwsKmsMrkAwareSymmetricDiscoveryKeyring({ + client, + discoveryFilter, + }) + ).to.throw('Client must be configured to a region') + }) + + it('Precondition: The AwsKmsMrkAwareSymmetricDiscoveryKeyring Discovery filter *must* be able to match something.', () => { + testAccountAndPartition([], undefined) + testAccountAndPartition([''], undefined) + testAccountAndPartition(undefined, '') + testAccountAndPartition(['', '123456789012'], 'aws') + testAccountAndPartition(['123456789012'], '') + testAccountAndPartition([''], 'aws') + testAccountAndPartition([], 'aws') + + function testAccountAndPartition(accountIDs: any, partition: any) { + const client: any = { config: { region: 'us-west-2' } } + + class TestAwsKmsMrkAwareSymmetricDiscoveryKeyring extends AwsKmsMrkAwareSymmetricDiscoveryKeyringClass( + Keyring as Newable> + ) {} + expect( + () => + new TestAwsKmsMrkAwareSymmetricDiscoveryKeyring({ + client, + discoveryFilter: { accountIDs, partition } as any, + }) + ).to.throw('A discovery filter must be able to match something.') + } + }) +}) diff --git a/modules/kms-keyring/test/kms_mrk_discovery_keyring.edk-order.test.ts b/modules/kms-keyring/test/kms_mrk_discovery_keyring.edk-order.test.ts new file mode 100644 index 000000000..f02a11234 --- /dev/null +++ b/modules/kms-keyring/test/kms_mrk_discovery_keyring.edk-order.test.ts @@ -0,0 +1,158 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +/* eslint-env mocha */ + +import chai from 'chai' +import chaiAsPromised from 'chai-as-promised' +import { AwsKmsMrkAwareSymmetricDiscoveryKeyringClass } from '../src/kms_mrk_discovery_keyring' +import { + NodeAlgorithmSuite, + AlgorithmSuiteIdentifier, + NodeDecryptionMaterial, + EncryptedDataKey, + Keyring, + needs, + Newable, +} from '@aws-crypto/material-management' +chai.use(chaiAsPromised) +const { expect } = chai + +describe('KmsMrkKeyring: decrypt EDK order', () => { + it('short circuit on the first success', async () => { + const context = { some: 'context' } + const suite = new NodeAlgorithmSuite( + AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16_HKDF_SHA256 + ) + const discoveryFilter = { accountIDs: ['123456789012'], partition: 'aws' } + + const { edk } = edkHelper() + const edks = [...Array(5)].map(() => edk) + const state = buildClientState(edks, suite) + class TestKmsMrkKeyring extends AwsKmsMrkAwareSymmetricDiscoveryKeyringClass( + Keyring as Newable> + ) {} + + const testKeyring = new TestKmsMrkKeyring({ + client: state.client, + discoveryFilter, + }) + + const material = await testKeyring.onDecrypt( + new NodeDecryptionMaterial(suite, context), + edks + ) + + expect(material.hasUnencryptedDataKey).to.equal(true) + expect(state.calls).to.equal(1) + }) + + it('errors should not halt, but also short circuit after success', async () => { + const context = { some: 'context' } + const suite = new NodeAlgorithmSuite( + AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16_HKDF_SHA256 + ) + const discoveryFilter = { accountIDs: ['123456789012'], partition: 'aws' } + + const { edk } = edkHelper() + const edks = [...Array(5)].map(() => edk) + const state = buildClientState(edks, suite, { failureCount: 1 }) + + class TestKmsMrkKeyring extends AwsKmsMrkAwareSymmetricDiscoveryKeyringClass( + Keyring as Newable> + ) {} + + const testKeyring = new TestKmsMrkKeyring({ + client: state.client, + discoveryFilter, + }) + + const material = await testKeyring.onDecrypt( + new NodeDecryptionMaterial(suite, context), + edks + ) + + expect(material.hasUnencryptedDataKey).to.equal(true) + expect(state.calls).to.equal(2) + }) + + it('only contact KMS for the single configured CMK', async () => { + const context = { some: 'context' } + const suite = new NodeAlgorithmSuite( + AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16_HKDF_SHA256 + ) + const discoveryFilter = { accountIDs: ['123456789012'], partition: 'aws' } + + const edks = ['not-aws', 'not-aws', 'aws'] + .map(edkHelper) + .map(({ edk }) => edk) + const state = buildClientState(edks, suite, { edkSuccessIndex: 2 }) + class TestKmsMrkKeyring extends AwsKmsMrkAwareSymmetricDiscoveryKeyringClass( + Keyring as Newable> + ) {} + + const testKeyring = new TestKmsMrkKeyring({ + client: state.client, + discoveryFilter, + }) + + const material = new NodeDecryptionMaterial(suite, context) + const result = await testKeyring.onDecrypt(material, edks) + + expect(result.hasUnencryptedDataKey).to.equal(true) + expect(state.calls).to.equal(1) + }) +}) + +function edkHelper(partition?: any) { + // Very dirty uuid "thing" + const keyId = [...Array(3)] + .map(() => Math.random().toString(16).slice(2)) + .join('') + const edk = new EncryptedDataKey({ + providerId: 'aws-kms', + providerInfo: `arn:${ + typeof partition === 'string' ? partition : 'aws' + }:kms:us-east-1:123456789012:key/${keyId}`, + encryptedDataKey: Buffer.alloc(5), + }) + + return { + keyId, + edk, + } +} + +function buildClientState( + edks: EncryptedDataKey[], + { keyLengthBytes }: NodeAlgorithmSuite, + { + failureCount = 0, + edkSuccessIndex, + }: { failureCount?: number; edkSuccessIndex?: number } = {} as any +) { + const clientState = { + client: { decrypt, config: { region: 'us-east-1' } } as any, + calls: 0, + } + + return clientState + + async function decrypt({ KeyId }: any) { + clientState.calls += 1 + const { calls } = clientState + // If I need to fail some of the filtered elements + needs(calls > failureCount, 'try again') + /* It may be that the list of EDKs will be flittered. + * in which case the success EDK + * the call count will not + * match the index of the intended EDK. + * In which case just use the one provided... + */ + expect(KeyId).to.equal(edks[edkSuccessIndex || calls - 1].providerInfo) + return { + Plaintext: new Uint8Array(keyLengthBytes), + KeyId, + } + } +} diff --git a/modules/kms-keyring/test/kms_mrk_discovery_keyring.ondecrypt.test.ts b/modules/kms-keyring/test/kms_mrk_discovery_keyring.ondecrypt.test.ts new file mode 100644 index 000000000..46687f6b1 --- /dev/null +++ b/modules/kms-keyring/test/kms_mrk_discovery_keyring.ondecrypt.test.ts @@ -0,0 +1,807 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +/* eslint-env mocha */ + +import chai from 'chai' +import chaiAsPromised from 'chai-as-promised' +import { AwsKmsMrkAwareSymmetricDiscoveryKeyringClass } from '../src/kms_mrk_discovery_keyring' +import { + NodeAlgorithmSuite, + AlgorithmSuiteIdentifier, + KeyringTraceFlag, + NodeDecryptionMaterial, + EncryptedDataKey, + Keyring, + Newable, +} from '@aws-crypto/material-management' +chai.use(chaiAsPromised) +const { expect } = chai + +describe('AwsKmsMrkAwareSymmetricDiscoveryKeyring: _onDecrypt', () => { + it('returns material', async () => { + const discoveryFilter = { accountIDs: ['2222222222222'], partition: 'aws' } + const keyId = + 'arn:aws:kms:us-east-1:2222222222222:key/mrk-4321abcd12ab34cd56ef1234567890ab' + const keyOtherRegion = + 'arn:aws:kms:us-west-2:2222222222222:key/mrk-4321abcd12ab34cd56ef1234567890ab' + const context = { some: 'context' } + const grantTokens = ['grant'] + const suite = new NodeAlgorithmSuite( + AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16 + ) + + function decrypt({ + KeyId, + CiphertextBlob, + EncryptionContext, + GrantTokens, + }: any) { + expect(KeyId).to.equal(keyId) + expect(EncryptionContext).to.deep.equal(context) + expect(GrantTokens).to.equal(grantTokens) + return { + Plaintext: new Uint8Array(suite.keyLengthBytes), + KeyId: Buffer.from(CiphertextBlob as Uint8Array).toString('utf8'), + } + } + const client: any = { decrypt, config: { region: 'us-east-1' } } + class TestAwsKmsMrkAwareSymmetricDiscoveryKeyring extends AwsKmsMrkAwareSymmetricDiscoveryKeyringClass( + Keyring as Newable> + ) {} + + const testKeyring = new TestAwsKmsMrkAwareSymmetricDiscoveryKeyring({ + client, + discoveryFilter, + grantTokens, + }) + + const edk = new EncryptedDataKey({ + providerId: 'aws-kms', + providerInfo: keyOtherRegion, + encryptedDataKey: Buffer.from(keyId), + }) + + const otherEDK = new EncryptedDataKey({ + providerId: 'other-provider', + providerInfo: keyId, + encryptedDataKey: Buffer.from(keyId), + }) + + const seedMaterial = new NodeDecryptionMaterial(suite, context) + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 + //= type=test + //# OnDecrypt MUST take decryption materials (structures.md#decryption- + //# materials) and a list of encrypted data keys + //# (structures.md#encrypted-data-key) as input. + const material = await testKeyring.onDecrypt(seedMaterial, [edk, otherEDK]) + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 + //= type=test + //# Since the response does satisfies these requirements then OnDecrypt + //# MUST do the following with the response: + expect(seedMaterial === material).to.equal(true) + expect(material.hasUnencryptedDataKey).to.equal(true) + + expect(material.keyringTrace).to.have.lengthOf(1) + const [traceDecrypt] = material.keyringTrace + expect(traceDecrypt.keyNamespace).to.equal('aws-kms') + expect(traceDecrypt.keyName).to.equal(keyId) + expect( + traceDecrypt.flags & KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY + ).to.equal(KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY) + expect( + traceDecrypt.flags & KeyringTraceFlag.WRAPPING_KEY_VERIFIED_ENC_CTX + ).to.equal(KeyringTraceFlag.WRAPPING_KEY_VERIFIED_ENC_CTX) + }) + + it('do not attempt to decrypt anything if we already a unencrypted data key.', async () => { + const client: any = { config: { region: 'temp' } } + class TestAwsKmsMrkAwareSymmetricDiscoveryKeyring extends AwsKmsMrkAwareSymmetricDiscoveryKeyringClass( + Keyring as Newable> + ) {} + + const testKeyring = new TestAwsKmsMrkAwareSymmetricDiscoveryKeyring({ + client, + }) + + const context = { some: 'context' } + const suite = new NodeAlgorithmSuite( + AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16 + ) + const seedMaterial = new NodeDecryptionMaterial( + suite, + context + ).setUnencryptedDataKey(new Uint8Array(suite.keyLengthBytes), { + keyNamespace: 'k', + keyName: 'k', + flags: KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY, + }) + + // The Provider info is malformed, + // if the keyring filters this, + // it should throw. + const edk = new EncryptedDataKey({ + providerId: 'aws-kms', + providerInfo: 'Not:an/arn', + encryptedDataKey: new Uint8Array(1), + }) + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 + //= type=test + //# If the decryption materials (structures.md#decryption-materials) + //# already contained a valid plaintext data key OnDecrypt MUST + //# immediately return the unmodified decryption materials + //# (structures.md#decryption-materials). + const material = await testKeyring.onDecrypt(seedMaterial, [edk]) + + expect(material === seedMaterial).to.equal(true) + }) + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 + //= type=test + //# For each encrypted data key in the filtered set, one at a time, the + //# OnDecrypt MUST attempt to decrypt the data key. + it('does not halt on decrypt errors', async () => { + const discoveryFilter = { accountIDs: ['2222222222222'], partition: 'aws' } + const keyId = + 'arn:aws:kms:us-east-1:2222222222222:key/mrk-4321abcd12ab34cd56ef1234567890ab' + const otherKeyId = + 'arn:aws:kms:us-east-1:2222222222222:key/mrk-0000abcd12ab34cd56ef1234567890ab' + const context = { some: 'context' } + const grantTokens = ['grant'] + const suite = new NodeAlgorithmSuite( + AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16 + ) + + let kmsCalls = 0 + let errorThrown = false + async function decrypt({ + KeyId, + // CiphertextBlob, + EncryptionContext, + GrantTokens, + }: any) { + if (kmsCalls === 0) { + expect(KeyId).to.equal(keyId) + kmsCalls += 1 + // This forces me to wait to throw an error + await new Promise((resolve) => setTimeout(resolve, 10)) + errorThrown = true + throw new Error('failed to decrypt') + } + // If this is not true, then we have attempted + // the next edk before the last key was attempted. + expect(errorThrown).to.equal(true) + expect(kmsCalls).to.equal(1) + expect(KeyId).to.equal(otherKeyId) + expect(EncryptionContext).to.deep.equal(context) + expect(GrantTokens).to.equal(grantTokens) + kmsCalls += 1 + return { + Plaintext: new Uint8Array(suite.keyLengthBytes), + KeyId: otherKeyId, + } + } + + const client: any = { decrypt, config: { region: 'us-east-1' } } + class TestAwsKmsMrkAwareSymmetricDiscoveryKeyring extends AwsKmsMrkAwareSymmetricDiscoveryKeyringClass( + Keyring as Newable> + ) {} + + const testKeyring = new TestAwsKmsMrkAwareSymmetricDiscoveryKeyring({ + client, + discoveryFilter, + grantTokens, + }) + + const edk1 = new EncryptedDataKey({ + providerId: 'aws-kms', + providerInfo: keyId, + encryptedDataKey: Buffer.from(keyId), + }) + + const edk2 = new EncryptedDataKey({ + providerId: 'aws-kms', + providerInfo: otherKeyId, + encryptedDataKey: Buffer.from(otherKeyId), + }) + + const material = await testKeyring.onDecrypt( + new NodeDecryptionMaterial(suite, context), + [edk1, edk2] + ) + + expect(material.hasUnencryptedDataKey).to.equal(true) + expect(material.keyringTrace).to.have.lengthOf(1) + }) + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 + //= type=test + //# The set of encrypted data keys MUST first be filtered to match this + //# keyring's configuration. + describe('Only attemts to decrypt EDKs that match its configuration', () => { + it('does not attempt to decrypt non-AWS KMS EDKs', async () => { + const discoveryFilter = { + accountIDs: ['2222222222222'], + partition: 'aws', + } + const keyId = + 'arn:aws:kms:us-east-1:2222222222222:key/mrk-4321abcd12ab34cd56ef1234567890ab' + const context = { some: 'context' } + const grantTokens = ['grant'] + const suite = new NodeAlgorithmSuite( + AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16 + ) + + let kmsCalled = false + function decrypt() { + kmsCalled = true + return { + Plaintext: new Uint8Array(suite.keyLengthBytes), + KeyId: keyId, + } + } + const client: any = { decrypt, config: { region: 'us-east-1' } } + class TestAwsKmsMrkAwareSymmetricDiscoveryKeyring extends AwsKmsMrkAwareSymmetricDiscoveryKeyringClass( + Keyring as Newable> + ) {} + + const testKeyring = new TestAwsKmsMrkAwareSymmetricDiscoveryKeyring({ + client, + discoveryFilter, + grantTokens, + }) + + const edk = new EncryptedDataKey({ + providerId: 'not aws kms edk', + providerInfo: keyId, + encryptedDataKey: Buffer.from(keyId), + }) + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 + //= type=test + //# * Its provider ID MUST exactly match the value "aws-kms". + await expect( + testKeyring.onDecrypt(new NodeDecryptionMaterial(suite, context), [edk]) + ).to.rejectedWith(Error, 'Unable to decrypt data key') + expect(kmsCalled).to.equal(false) + }) + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 + //= type=test + //# * The provider info MUST be a valid AWS KMS ARN (aws-kms-key- + //# arn.md#a-valid-aws-kms-arn) with a resource type of "key" or + //# OnDecrypt MUST fail. + describe('halts and throws an error if encounters aws-kms EDK ProviderInfo with', () => { + const client: any = { config: { region: 'us-east-1' } } + const discoveryFilter = { + accountIDs: ['2222222222222'], + partition: 'aws', + } + const context = { some: 'context' } + const grantTokens = ['grant'] + const suite = new NodeAlgorithmSuite( + AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16 + ) + + class TestAwsKmsMrkAwareSymmetricDiscoveryKeyring extends AwsKmsMrkAwareSymmetricDiscoveryKeyringClass( + Keyring as Newable> + ) {} + + it('a non-valid keyId', async () => { + const keyId = 'Not:an/arn' + + const testKeyring = new TestAwsKmsMrkAwareSymmetricDiscoveryKeyring({ + client, + discoveryFilter, + grantTokens, + }) + + const edk = new EncryptedDataKey({ + providerId: 'aws-kms', + providerInfo: keyId, + encryptedDataKey: Buffer.from(keyId), + }) + + return expect( + testKeyring.onDecrypt(new NodeDecryptionMaterial(suite, context), [ + edk, + ]) + ).to.rejectedWith(Error, 'Malformed arn') + }) + + it('raw key id', async () => { + const keyId = 'mrk-80bd8ecdcd4342aebd84b7dc9da498a7' + + const testKeyring = new TestAwsKmsMrkAwareSymmetricDiscoveryKeyring({ + client, + discoveryFilter, + grantTokens, + }) + + const edk = new EncryptedDataKey({ + providerId: 'aws-kms', + providerInfo: keyId, + encryptedDataKey: Buffer.from(keyId), + }) + + return expect( + testKeyring.onDecrypt(new NodeDecryptionMaterial(suite, context), [ + edk, + ]) + ).to.rejectedWith(Error, 'Unexpected EDK ProviderInfo for AWS KMS EDK') + }) + + it('a alias ARN', async () => { + const keyId = 'arn:aws:kms:us-west-2:658956600833:alias/EncryptDecrypt' + + const testKeyring = new TestAwsKmsMrkAwareSymmetricDiscoveryKeyring({ + client, + discoveryFilter, + grantTokens, + }) + + const edk = new EncryptedDataKey({ + providerId: 'aws-kms', + providerInfo: keyId, + encryptedDataKey: Buffer.from(keyId), + }) + + return expect( + testKeyring.onDecrypt(new NodeDecryptionMaterial(suite, context), [ + edk, + ]) + ).to.rejectedWith(Error, 'Unexpected EDK ProviderInfo for AWS KMS EDK') + }) + }) + + describe('does not attempt to decrypt CMKs which do not match discovery filter', () => { + const keyId = + 'arn:aws:kms:us-east-1:2222222222222:key/mrk-4321abcd12ab34cd56ef1234567890ab' + const context = { some: 'context' } + const grantTokens = ['grant'] + const suite = new NodeAlgorithmSuite( + AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16 + ) + + // This works because both these do NOT expect to call KMS + let kmsCalled = false + function decrypt() { + kmsCalled = true + return { + Plaintext: new Uint8Array(suite.keyLengthBytes), + KeyId: keyId, + } + } + const client: any = { decrypt, config: { region: 'us-east-1' } } + class TestAwsKmsMrkAwareSymmetricDiscoveryKeyring extends AwsKmsMrkAwareSymmetricDiscoveryKeyringClass( + Keyring as Newable> + ) {} + + const edk = new EncryptedDataKey({ + providerId: 'aws-kms', + providerInfo: keyId, + encryptedDataKey: Buffer.from(keyId), + }) + + it('according to accountID', async () => { + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 + //= type=test + //# * If a discovery filter is configured, its set of accounts MUST + //# contain the provider info account. + await expect( + new TestAwsKmsMrkAwareSymmetricDiscoveryKeyring({ + client, + grantTokens, + discoveryFilter: { + accountIDs: ['Not: 2222222222222'], + partition: 'aws', + }, + }).onDecrypt(new NodeDecryptionMaterial(suite, context), [edk]) + ).to.rejectedWith(Error, 'Unable to decrypt data key') + expect(kmsCalled).to.equal(false) + }) + + it('according to partition', async () => { + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 + //= type=test + //# * If a discovery filter is configured, its partition and the + //# provider info partition MUST match. + await expect( + new TestAwsKmsMrkAwareSymmetricDiscoveryKeyring({ + client, + grantTokens, + discoveryFilter: { + accountIDs: ['2222222222222'], + partition: 'notAWS', + }, + }).onDecrypt(new NodeDecryptionMaterial(suite, context), [edk]) + ).to.rejectedWith(Error, 'Unable to decrypt data key') + expect(kmsCalled).to.equal(false) + }) + }) + + it('does not attempt to decrypt non-MRK CMK ARNs if it is not in our region', async () => { + const discoveryFilter = { + accountIDs: ['2222222222222'], + partition: 'aws', + } + const keyId = + 'arn:aws:kms:us-east-2:2222222222222:key/4321abcd12ab34cd56ef1234567890ab' + const context = { some: 'context' } + const grantTokens = ['grant'] + const suite = new NodeAlgorithmSuite( + AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16 + ) + + let kmsCalled = false + function decrypt() { + kmsCalled = true + return { + Plaintext: new Uint8Array(suite.keyLengthBytes), + KeyId: keyId, + } + } + const client: any = { decrypt, config: { region: 'us-east-1' } } + class TestAwsKmsMrkAwareSymmetricDiscoveryKeyring extends AwsKmsMrkAwareSymmetricDiscoveryKeyringClass( + Keyring as Newable> + ) {} + + const edk = new EncryptedDataKey({ + providerId: 'aws-kms', + providerInfo: keyId, + encryptedDataKey: Buffer.from(keyId), + }) + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 + //= type=test + //# * If the provider info is not identified as a multi-Region key (aws- + //# kms-key-arn.md#identifying-an-aws-kms-multi-region-key), then the + //# provider info's Region MUST match the AWS KMS client region. + await expect( + new TestAwsKmsMrkAwareSymmetricDiscoveryKeyring({ + client, + grantTokens, + discoveryFilter, + }).onDecrypt(new NodeDecryptionMaterial(suite, context), [edk]) + ).to.rejectedWith(Error, 'Unable to decrypt data key') + expect(kmsCalled).to.equal(false) + }) + }) + + it('calls KMS Decrypt with keyId as an ARN indicating the configured region if an MRK-indicating ARN', async () => { + const discoveryFilter = { accountIDs: ['2222222222222'], partition: 'aws' } + const usWest2Key = + 'arn:aws:kms:us-west-2:2222222222222:key/mrk-4321abcd12ab34cd56ef1234567890ab' + const usEast1Key = + 'arn:aws:kms:us-east-1:2222222222222:key/mrk-4321abcd12ab34cd56ef1234567890ab' + const encryptedDataKey = Buffer.from(usEast1Key) + const context = { some: 'context' } + const grantTokens = ['grant'] + const suite = new NodeAlgorithmSuite( + AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16 + ) + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 + //= type=test + //# To attempt to decrypt a particular encrypted data key + //# (structures.md#encrypted-data-key), OnDecrypt MUST call AWS KMS + //# Decrypt (https://docs.aws.amazon.com/kms/latest/APIReference/ + //# API_Decrypt.html) with the configured AWS KMS client. + // + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 + //= type=test + //# When calling AWS KMS Decrypt + //# (https://docs.aws.amazon.com/kms/latest/APIReference/ + //# API_Decrypt.html), the keyring MUST call with a request constructed + //# as follows: + function decrypt({ + KeyId, + EncryptionContext, + GrantTokens, + CiphertextBlob, + }: any) { + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 + //= type=test + //# * "KeyId": If the provider info's resource type is "key" and its + //# resource is a multi-Region key then a new ARN MUST be created + //# where the region part MUST equal the AWS KMS client region and + //# every other part MUST equal the provider info. + expect(KeyId).to.equal(usWest2Key) + expect(CiphertextBlob).to.deep.equal(encryptedDataKey) + expect(EncryptionContext).to.deep.equal(context) + expect(GrantTokens).to.equal(grantTokens) + return { + Plaintext: new Uint8Array(suite.keyLengthBytes), + KeyId, + } + } + const client: any = { decrypt, config: { region: 'us-west-2' } } + class TestAwsKmsMrkAwareSymmetricDiscoveryKeyring extends AwsKmsMrkAwareSymmetricDiscoveryKeyringClass( + Keyring as Newable> + ) {} + + const testKeyring = new TestAwsKmsMrkAwareSymmetricDiscoveryKeyring({ + client, + discoveryFilter, + grantTokens, + }) + + const edk = new EncryptedDataKey({ + providerId: 'aws-kms', + providerInfo: usEast1Key, + encryptedDataKey, + }) + + const material = await testKeyring.onDecrypt( + new NodeDecryptionMaterial(suite, context), + [edk] + ) + + expect(material.hasUnencryptedDataKey).to.equal(true) + }) + + it('calls KMS Decrypt with keyId as the exact ARN if not an MRK ARN', async () => { + const discoveryFilter = { accountIDs: ['2222222222222'], partition: 'aws' } + const keyId = + 'arn:aws:kms:us-west-2:2222222222222:key/4321abcd12ab34cd56ef1234567890ab' + const context = { some: 'context' } + const grantTokens = ['grant'] + const suite = new NodeAlgorithmSuite( + AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16 + ) + + function decrypt({ + KeyId, + CiphertextBlob, + EncryptionContext, + GrantTokens, + }: any) { + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 + //= type=test + //# Otherwise it MUST + //# be the provider info. + expect(KeyId).to.equal(keyId) + expect(EncryptionContext).to.deep.equal(context) + expect(GrantTokens).to.equal(grantTokens) + return { + Plaintext: new Uint8Array(suite.keyLengthBytes), + KeyId: Buffer.from(CiphertextBlob as Uint8Array).toString('utf8'), + } + } + const client: any = { decrypt, config: { region: 'us-west-2' } } + class TestAwsKmsMrkAwareSymmetricDiscoveryKeyring extends AwsKmsMrkAwareSymmetricDiscoveryKeyringClass( + Keyring as Newable> + ) {} + + const testKeyring = new TestAwsKmsMrkAwareSymmetricDiscoveryKeyring({ + client, + discoveryFilter, + grantTokens, + }) + + const edk = new EncryptedDataKey({ + providerId: 'aws-kms', + providerInfo: keyId, + encryptedDataKey: Buffer.from(keyId), + }) + + const material = await testKeyring.onDecrypt( + new NodeDecryptionMaterial(suite, context), + [edk] + ) + + expect(material.hasUnencryptedDataKey).to.equal(true) + }) + + it('collects an error if the KeyId from KMS Decrypt does not match the requested KeyID.', async () => { + const discoveryFilter = { accountIDs: ['2222222222222'], partition: 'aws' } + const keyId = + 'arn:aws:kms:us-east-1:2222222222222:key/mrk-4321abcd12ab34cd56ef1234567890ab' + const context = { some: 'context' } + const grantTokens = ['grant'] + const suite = new NodeAlgorithmSuite( + AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16 + ) + + function decrypt({ KeyId, EncryptionContext }: any) { + expect(EncryptionContext).to.deep.equal(context) + expect(KeyId).to.equal(keyId) + return { + Plaintext: new Uint8Array(suite.keyLengthBytes), + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 + //= type=test + //# * The "KeyId" field in the response MUST equal the requested "KeyId" + KeyId: 'Not the right ARN', + } + } + const client: any = { decrypt, config: { region: 'us-east-1' } } + class TestAwsKmsMrkAwareSymmetricDiscoveryKeyring extends AwsKmsMrkAwareSymmetricDiscoveryKeyringClass( + Keyring as Newable> + ) {} + + const testKeyring = new TestAwsKmsMrkAwareSymmetricDiscoveryKeyring({ + client, + discoveryFilter, + grantTokens, + }) + + const edk = new EncryptedDataKey({ + providerId: 'aws-kms', + providerInfo: keyId, + encryptedDataKey: Buffer.from(keyId), + }) + + return expect( + testKeyring.onDecrypt(new NodeDecryptionMaterial(suite, context), [edk]) + ).to.rejectedWith( + Error, + 'KMS Decryption key does not match the requested key id.' + ) + }) + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 + //= type=test + //# If the response does not satisfies these requirements then an error + //# is collected and the next encrypted data key in the filtered set MUST + //# be attempted. + it('collects an error if the decrypted unencryptedDataKey length does not match the algorithm specification.', async () => { + const discoveryFilter = { accountIDs: ['2222222222222'], partition: 'aws' } + const keyId = + 'arn:aws:kms:us-east-1:2222222222222:key/mrk-4321abcd12ab34cd56ef1234567890ab' + const context = { some: 'context' } + const grantTokens = ['grant'] + const suite = new NodeAlgorithmSuite( + AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16 + ) + let awsKmsCalled = 0 + + function decrypt({ CiphertextBlob, EncryptionContext, GrantTokens }: any) { + awsKmsCalled += 1 + expect(EncryptionContext).to.deep.equal(context) + expect(GrantTokens).to.equal(grantTokens) + return { + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 + //= type=test + //# * The length of the response's "Plaintext" MUST equal the key + //# derivation input length (algorithm-suites.md#key-derivation-input- + //# length) specified by the algorithm suite (algorithm-suites.md) + //# included in the input decryption materials + //# (structures.md#decryption-materials). + Plaintext: new Uint8Array(suite.keyLengthBytes - 5), + KeyId: Buffer.from(CiphertextBlob as Uint8Array).toString('utf8'), + } + } + const client: any = { decrypt, config: { region: 'us-east-1' } } + class TestAwsKmsMrkAwareSymmetricDiscoveryKeyring extends AwsKmsMrkAwareSymmetricDiscoveryKeyringClass( + Keyring as Newable> + ) {} + + const testKeyring = new TestAwsKmsMrkAwareSymmetricDiscoveryKeyring({ + client, + discoveryFilter, + grantTokens, + }) + + const edk1 = new EncryptedDataKey({ + providerId: 'aws-kms', + providerInfo: keyId, + encryptedDataKey: Buffer.from(keyId), + }) + + const edk2 = new EncryptedDataKey({ + providerId: 'aws-kms', + providerInfo: keyId, + encryptedDataKey: Buffer.from(keyId), + }) + + await expect( + testKeyring.onDecrypt(new NodeDecryptionMaterial(suite, context), [ + edk1, + edk2, + ]) + ).to.rejectedWith( + Error, + 'Key length does not agree with the algorithm specification.' + ) + + expect(awsKmsCalled).to.equal(2) + }) + + describe('throws an error if does not successfully decrypt any EDK', () => { + it('because it encountered no EDKs to decrypt', async () => { + const discoveryFilter = { + accountIDs: ['2222222222222'], + partition: 'aws', + } + const context = { some: 'context' } + const grantTokens = ['grant'] + const suite = new NodeAlgorithmSuite( + AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16 + ) + + const client: any = { config: { region: 'us-east-1' } } + class TestAwsKmsMrkAwareSymmetricDiscoveryKeyring extends AwsKmsMrkAwareSymmetricDiscoveryKeyringClass( + Keyring as Newable> + ) {} + + const testKeyring = new TestAwsKmsMrkAwareSymmetricDiscoveryKeyring({ + client, + discoveryFilter, + grantTokens, + }) + + await expect( + testKeyring.onDecrypt(new NodeDecryptionMaterial(suite, context), []) + ).to.rejectedWith(Error, 'Unable to decrypt data key: No EDKs supplied.') + }) + + it('because it encountered decryption errors', async () => { + const discoveryFilter = { + accountIDs: ['2222222222222'], + partition: 'aws', + } + const keyId = + 'arn:aws:kms:us-east-1:2222222222222:key/mrk-4321abcd12ab34cd56ef1234567890ab' + const otherKeyId = + 'arn:aws:kms:us-east-1:2222222222222:key/mrk-0000abcd12ab34cd56ef1234567890ab' + const context = { some: 'context' } + const grantTokens = ['grant'] + const suite = new NodeAlgorithmSuite( + AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16 + ) + + let edkCount = 0 + function decrypt({ KeyId }: any) { + if (edkCount === 0) { + expect(KeyId).to.equal(keyId) + edkCount += 1 + throw new Error('Decrypt Error 1') + } else { + expect(KeyId).to.equal(otherKeyId) + edkCount += 1 + throw new Error('Decrypt Error 2') + } + } + const client: any = { decrypt, config: { region: 'us-east-1' } } + class TestAwsKmsMrkAwareSymmetricDiscoveryKeyring extends AwsKmsMrkAwareSymmetricDiscoveryKeyringClass( + Keyring as Newable> + ) {} + + const testKeyring = new TestAwsKmsMrkAwareSymmetricDiscoveryKeyring({ + client, + discoveryFilter, + grantTokens, + }) + + const edk1 = new EncryptedDataKey({ + providerId: 'aws-kms', + providerInfo: keyId, + encryptedDataKey: Buffer.from(keyId), + }) + + const edk2 = new EncryptedDataKey({ + providerId: 'aws-kms', + providerInfo: otherKeyId, + encryptedDataKey: Buffer.from(otherKeyId), + }) + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 + //= type=test + //# If OnDecrypt fails to successfully decrypt any encrypted data key + //# (structures.md#encrypted-data-key), then it MUST yield an error that + //# includes all collected errors. + await expect( + testKeyring.onDecrypt(new NodeDecryptionMaterial(suite, context), [ + edk1, + edk2, + ]) + ).to.rejectedWith( + Error, + /Unable to decrypt data key[\s\S]*Decrypt Error 1[\s\S]*Decrypt Error 2/ + ) + }) + }) +}) diff --git a/modules/kms-keyring/test/kms_mrk_discovery_keyring.onencrypt.test.ts b/modules/kms-keyring/test/kms_mrk_discovery_keyring.onencrypt.test.ts new file mode 100644 index 000000000..5e0817bb3 --- /dev/null +++ b/modules/kms-keyring/test/kms_mrk_discovery_keyring.onencrypt.test.ts @@ -0,0 +1,47 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +/* eslint-env mocha */ + +import chai from 'chai' +import chaiAsPromised from 'chai-as-promised' +import { + NodeAlgorithmSuite, + NodeEncryptionMaterial, + Keyring, + AlgorithmSuiteIdentifier, + Newable, +} from '@aws-crypto/material-management' +import { AwsKmsMrkAwareSymmetricDiscoveryKeyringClass } from '../src/kms_mrk_discovery_keyring' +chai.use(chaiAsPromised) +const { expect } = chai + +describe('AwsKmsMrkAwareSymmetricKeyring: _onEncrypt', () => { + it('Encrypt returns an error.', async () => { + const client: any = { config: { region: 'us-west-2' } } + const discoveryFilter = { accountIDs: ['123456789012'], partition: 'aws' } + const context = { some: 'context' } + const suite = new NodeAlgorithmSuite( + AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16_HKDF_SHA256 + ) + + class TestKmsMrkKeyring extends AwsKmsMrkAwareSymmetricDiscoveryKeyringClass( + Keyring as Newable> + ) {} + + const testKeyring = new TestKmsMrkKeyring({ + client, + discoveryFilter, + }) + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.7 + //= type=test + //# This function MUST fail. + return expect( + testKeyring.onEncrypt(new NodeEncryptionMaterial(suite, context)) + ).to.rejectedWith( + Error, + 'AwsKmsMrkAwareSymmetricDiscoveryKeyring cannot be used to encrypt' + ) + }) +}) diff --git a/modules/kms-keyring/test/kms_mrk_discovery_multi_keyring.test.ts b/modules/kms-keyring/test/kms_mrk_discovery_multi_keyring.test.ts new file mode 100644 index 000000000..e82520ab3 --- /dev/null +++ b/modules/kms-keyring/test/kms_mrk_discovery_multi_keyring.test.ts @@ -0,0 +1,157 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +/* eslint-env mocha */ + +import { expect } from 'chai' +import { getAwsKmsMrkAwareDiscoveryMultiKeyringBuilder } from '../src/kms_mrk_discovery_multi_keyring' +import { AwsKmsMrkAwareSymmetricDiscoveryKeyringClass } from '../src/kms_mrk_discovery_keyring' +import { + MultiKeyringNode, + Newable, + KeyringNode, +} from '@aws-crypto/material-management' + +describe('buildAwsKmsMrkAwareStrictMultiKeyringNode', () => { + class TestMrkAwareSymmetricKeyring extends AwsKmsMrkAwareSymmetricDiscoveryKeyringClass( + KeyringNode as Newable + ) {} + + const testBuilder = getAwsKmsMrkAwareDiscoveryMultiKeyringBuilder( + TestMrkAwareSymmetricKeyring, + MultiKeyringNode, + (region: string): any => { + return { config: { region } } + } + ) + it('constructs expected child/generator keyrings', async () => { + const regions = ['us-west-2', 'us-east-1'] + const discoveryFilter = { partition: 'aws', accountIDs: ['1234'] } + const grantTokens = ['grant', 'tokens'] + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.5 + //= type=test + //# The caller MUST provide: + const test = testBuilder({ + discoveryFilter, + regions, + clientProvider(region: string): any { + //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.5 + //= type=test + //# A set of AWS KMS clients MUST be created by calling regional client + //# supplier for each region in the input set of regions. + expect(region.includes(region)).to.equal(true) + return { config: { region } } + }, + grantTokens, + }) + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.5 + //= type=test + //# Then a Multi-Keyring (../multi-keyring.md#inputs) MUST be initialize + //# by using this set of discovery keyrings as the child keyrings + //# (../multi-keyring.md#child-keyrings). + // + //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.5 + //= type=test + //# This Multi-Keyring MUST be + //# this functions output. + expect(test instanceof MultiKeyringNode).to.equal(true) + + expect(!!test.generator).to.equal(false) + //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.5 + //= type=test + //# Then a set of AWS KMS MRK Aware Symmetric Region Discovery Keyring + //# (aws-kms-mrk-aware-symmetric-region-discovery-keyring.md) MUST be + //# created for each AWS KMS client by initializing each keyring with + expect(test.children).to.have.lengthOf(2) + expect(test.children[0] instanceof TestMrkAwareSymmetricKeyring).to.equal( + true + ) + expect(test.children[1] instanceof TestMrkAwareSymmetricKeyring).to.equal( + true + ) + const child1 = test.children[0] as TestMrkAwareSymmetricKeyring + const child2 = test.children[1] as TestMrkAwareSymmetricKeyring + expect(!!child1.client).to.equal(true) + expect(!!child2.client).to.equal(true) + expect( + // @ts-ignore the V3 client has set the config to protected + child1.client.config.region + ).to.equal('us-west-2') + expect( + // @ts-ignore the V3 client has set the config to protected + child2.client.config.region + ).to.equal('us-east-1') + + expect(child1.discoveryFilter).to.deep.equal(discoveryFilter) + expect(child2.discoveryFilter).to.deep.equal(discoveryFilter) + expect(child1.grantTokens).to.deep.equal(grantTokens) + expect(child2.grantTokens).to.deep.equal(grantTokens) + }) + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.5 + //= type=test + //# If a regional client supplier is not passed, + //# then a default MUST be created that takes a region string and + //# generates a default AWS SDK client for the given region. + it('Can create clients from the default provider.', () => { + const regions = ['us-west-2', 'us-east-1'] + const test = testBuilder({ regions }) + expect(test instanceof MultiKeyringNode).to.equal(true) + expect( + // @ts-ignore the V3 client has set the config to protected + test.children[0].client.config.region + ).to.equal('us-west-2') + expect( + // @ts-ignore the V3 client has set the config to protected + test.children[1].client.config.region + ).to.equal('us-east-1') + }) + + it('If an empty set of Region is provided this function MUST fail.', async () => { + //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.5 + //= type=test + //# If an empty set of Region is provided this function MUST fail. + expect(() => testBuilder({ regions: [] })).to.throw( + Error, + 'Configured regions must contain at least one region.' + ) + }) + + it('If any element of the set of regions is null or an empty string this function MUST fail.', async () => { + //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.5 + //= type=test + //# If + //# any element of the set of regions is null or an empty string this + //# function MUST fail. + expect(() => + testBuilder({ + regions: ['us-west-2', ''], + }) + ).to.throw( + Error, + 'Configured regions must not contain a null or empty string as a region.' + ) + expect(() => + testBuilder({ + // @ts-expect-error undefined is not a string + regions: ['us-west-2', undefined], + }) + ).to.throw( + Error, + 'Configured regions must not contain a null or empty string as a region.' + ) + }) + + it('Postcondition: If the configured clientProvider is not able to create a client for a defined region, throw an error.', async () => { + const regions = ['us-west-2', 'us-east-1'] + const clientProvider: any = () => { + return false + } + expect(() => testBuilder({ clientProvider, regions })).to.throw( + Error, + 'Configured clientProvider is unable to create a client for a configured region.' + ) + }) +}) diff --git a/modules/kms-keyring/test/kms_mrk_keyring.constructor.test.ts b/modules/kms-keyring/test/kms_mrk_keyring.constructor.test.ts new file mode 100644 index 000000000..dac37d415 --- /dev/null +++ b/modules/kms-keyring/test/kms_mrk_keyring.constructor.test.ts @@ -0,0 +1,140 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +/* eslint-env mocha */ + +import { expect } from 'chai' +import { AwsKmsMrkAwareSymmetricKeyringClass } from '../src/kms_mrk_keyring' +import { + NodeAlgorithmSuite, + Keyring, + Newable, +} from '@aws-crypto/material-management' + +describe('AwsKmsMrkAwareSymmetricKeyring: constructor', () => { + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.6 + //= type=test + //# On initialization the caller MUST provide: + it('set properties', () => { + const client: any = {} + const keyId = 'arn:aws:kms:us-east-1:123456789012:alias/example-alias' + const grantTokens = ['grant'] + + class TestAwsKmsMrkAwareSymmetricKeyring extends AwsKmsMrkAwareSymmetricKeyringClass( + Keyring as Newable> + ) {} + + const test = new TestAwsKmsMrkAwareSymmetricKeyring({ + client, + keyId, + grantTokens, + }) + expect(test.client).to.equal(client) + expect(test.keyId).to.equal(keyId) + expect(test.grantTokens).to.equal(grantTokens) + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.5 + //= type=test + //# MUST implement the AWS Encryption SDK Keyring interface (../keyring- + //# interface.md#interface) + expect((test as Keyring) instanceof Keyring).to.equal( + true + ) + }) + + it('Configured KMS key identifier must be valid.', () => { + const client: any = {} + class TestAwsKmsMrkAwareSymmetricKeyring extends AwsKmsMrkAwareSymmetricKeyringClass( + Keyring as Newable> + ) {} + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.6 + //= type=test + //# The AWS KMS key identifier MUST NOT be null or empty. + expect( + () => + new TestAwsKmsMrkAwareSymmetricKeyring({ + client, + keyId: '', + }) + ).to.throw('An AWS KMS key identifier is required.') + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.6 + //= type=test + //# The AWS KMS + //# key identifier MUST be a valid identifier (aws-kms-key-arn.md#a- + //# valid-aws-kms-identifier). + expect( + () => + new TestAwsKmsMrkAwareSymmetricKeyring({ + client, + keyId: 'Not:an/arn', + }) + ).to.throw('Malformed arn.') + expect( + () => + new TestAwsKmsMrkAwareSymmetricKeyring({ + client, + // @ts-expect-error passing undefined + keyId: undefined, + }) + ).to.throw('An AWS KMS key identifier is required.') + + expect( + () => + new TestAwsKmsMrkAwareSymmetricKeyring({ + client, + // @ts-expect-error passing null + keyId: null, + }) + ).to.throw('An AWS KMS key identifier is required.') + + expect( + () => + new TestAwsKmsMrkAwareSymmetricKeyring({ + client, + // @ts-expect-error passing a number to expect failure + keyId: 5, + }) + ).to.throw('An AWS KMS key identifier is required.') + + expect( + () => + new TestAwsKmsMrkAwareSymmetricKeyring({ + client, + // @ts-expect-error passing an object to expect failure + keyId: {}, + }) + ).to.throw('An AWS KMS key identifier is required.') + }) + + it('A KMS CMK alias is a valid CMK identifier', () => { + const client: any = {} + class TestAwsKmsMrkAwareSymmetricKeyring extends AwsKmsMrkAwareSymmetricKeyringClass( + Keyring as Newable> + ) {} + + const test2 = new TestAwsKmsMrkAwareSymmetricKeyring({ + client, + keyId: 'alias/example-alias', + }) + expect(test2).to.be.instanceOf(TestAwsKmsMrkAwareSymmetricKeyring) + }) + + it('provide a client', () => { + class TestAwsKmsMrkAwareSymmetricKeyring extends AwsKmsMrkAwareSymmetricKeyringClass( + Keyring as Newable> + ) {} + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.6 + //= type=test + //# The AWS KMS + //# SDK client MUST NOT be null. + expect( + () => + new TestAwsKmsMrkAwareSymmetricKeyring({ + // @ts-expect-error testing that I get an error + client: false, + keyId: 'alias/example-alias', + }) + ).to.throw('An AWS SDK client is required') + }) +}) diff --git a/modules/kms-keyring/test/kms_mrk_keyring.edk-order.test.ts b/modules/kms-keyring/test/kms_mrk_keyring.edk-order.test.ts new file mode 100644 index 000000000..e1ecb14b7 --- /dev/null +++ b/modules/kms-keyring/test/kms_mrk_keyring.edk-order.test.ts @@ -0,0 +1,154 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +/* eslint-env mocha */ + +import chai from 'chai' +import chaiAsPromised from 'chai-as-promised' +import { AwsKmsMrkAwareSymmetricKeyringClass } from '../src/kms_mrk_keyring' +import { + NodeAlgorithmSuite, + AlgorithmSuiteIdentifier, + NodeDecryptionMaterial, + EncryptedDataKey, + Keyring, + needs, + Newable, +} from '@aws-crypto/material-management' +chai.use(chaiAsPromised) +const { expect } = chai + +describe('KmsMrkKeyring: decrypt EDK order', () => { + it('short circuit on the first success', async () => { + const context = { some: 'context' } + const suite = new NodeAlgorithmSuite( + AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16_HKDF_SHA256 + ) + + const { edk } = edkHelper() + const edks = [...Array(5)].map(() => edk) + const state = buildClientState(edks, suite) + class TestKmsMrkKeyring extends AwsKmsMrkAwareSymmetricKeyringClass( + Keyring as Newable> + ) {} + + const testKeyring = new TestKmsMrkKeyring({ + client: state.client, + keyId: edk.providerInfo, + }) + + const material = await testKeyring.onDecrypt( + new NodeDecryptionMaterial(suite, context), + edks + ) + + expect(material.hasUnencryptedDataKey).to.equal(true) + expect(state.calls).to.equal(1) + }) + + it('errors should not halt, but also short circuit after success', async () => { + const context = { some: 'context' } + const suite = new NodeAlgorithmSuite( + AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16_HKDF_SHA256 + ) + + const { edk } = edkHelper() + const edks = [...Array(5)].map(() => edk) + const state = buildClientState(edks, suite, { failureCount: 1 }) + + class TestKmsMrkKeyring extends AwsKmsMrkAwareSymmetricKeyringClass( + Keyring as Newable> + ) {} + + const testKeyring = new TestKmsMrkKeyring({ + client: state.client, + keyId: edk.providerInfo, + }) + + const material = await testKeyring.onDecrypt( + new NodeDecryptionMaterial(suite, context), + edks + ) + + expect(material.hasUnencryptedDataKey).to.equal(true) + expect(state.calls).to.equal(2) + }) + + it('only contact KMS for the single configured CMK', async () => { + const context = { some: 'context' } + const suite = new NodeAlgorithmSuite( + AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16_HKDF_SHA256 + ) + + const edks = [...Array(5)].map(edkHelper).map(({ edk }) => edk) + const state = buildClientState(edks, suite, { edkSuccessIndex: 4 }) + class TestKmsMrkKeyring extends AwsKmsMrkAwareSymmetricKeyringClass( + Keyring as Newable> + ) {} + + const testKeyring = new TestKmsMrkKeyring({ + client: state.client, + keyId: edks[edks.length - 1].providerInfo, + }) + + const material = await testKeyring.onDecrypt( + new NodeDecryptionMaterial(suite, context), + edks + ) + + expect(material.hasUnencryptedDataKey).to.equal(true) + expect(state.calls).to.equal(1) + }) +}) + +function edkHelper(partition?: any) { + // Very dirty uuid "thing" + const keyId = [...Array(3)] + .map(() => Math.random().toString(16).slice(2)) + .join('') + const accountId = Math.random().toString().slice(2, 14) + const edk = new EncryptedDataKey({ + providerId: 'aws-kms', + providerInfo: `arn:${ + typeof partition === 'string' ? partition : 'aws' + }:kms:us-east-1:${accountId}:key/${keyId}`, + encryptedDataKey: Buffer.alloc(5), + }) + + return { + keyId, + accountId, + edk, + } +} + +function buildClientState( + edks: EncryptedDataKey[], + { keyLengthBytes }: NodeAlgorithmSuite, + { + failureCount = 0, + edkSuccessIndex, + }: { failureCount?: number; edkSuccessIndex?: number } = {} as any +) { + const clientState = { client: { decrypt } as any, calls: 0 } + + return clientState + + async function decrypt({ KeyId }: any) { + clientState.calls += 1 + const { calls } = clientState + // If I need to fail some of the filtered elements + needs(calls > failureCount, 'try again') + /* It may be that the list of EDKs will be flittered. + * in which case the success EDK + * the call count will not + * match the index of the intended EDK. + * In which case just use the one provided... + */ + expect(KeyId).to.equal(edks[edkSuccessIndex || calls - 1].providerInfo) + return { + Plaintext: new Uint8Array(keyLengthBytes), + KeyId, + } + } +} diff --git a/modules/kms-keyring/test/kms_mrk_keyring.ondecrypt.test.ts b/modules/kms-keyring/test/kms_mrk_keyring.ondecrypt.test.ts new file mode 100644 index 000000000..836b84f8f --- /dev/null +++ b/modules/kms-keyring/test/kms_mrk_keyring.ondecrypt.test.ts @@ -0,0 +1,653 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +/* eslint-env mocha */ + +import chai from 'chai' +import chaiAsPromised from 'chai-as-promised' +import { AwsKmsMrkAwareSymmetricKeyringClass } from '../src/kms_mrk_keyring' +import { + NodeAlgorithmSuite, + AlgorithmSuiteIdentifier, + KeyringTraceFlag, + NodeDecryptionMaterial, + EncryptedDataKey, + Keyring, + Newable, +} from '@aws-crypto/material-management' +chai.use(chaiAsPromised) +const { expect } = chai + +describe('AwsKmsMrkAwareSymmetricKeyring: _onDecrypt', () => { + describe('returns material', () => { + it('for configured MRK ARN', async () => { + const configuredKeyId = + 'arn:aws:kms:us-east-1:2222222222222:key/mrk-4321abcd12ab34cd56ef1234567890ab' + const keyIdOtherRegion = + 'arn:aws:kms:us-west-2:2222222222222:key/mrk-4321abcd12ab34cd56ef1234567890ab' + const context = { some: 'context' } + const grantTokens = ['grant'] + const suite = new NodeAlgorithmSuite( + AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16 + ) + + function decrypt({ + KeyId, + CiphertextBlob, + EncryptionContext, + GrantTokens, + }: any) { + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.8 + //= type=test + //# When calling AWS KMS Decrypt + //# (https://docs.aws.amazon.com/kms/latest/APIReference/ + //# API_Decrypt.html), the keyring MUST call with a request constructed + //# as follows: + expect(KeyId).to.equal(configuredKeyId) + expect(EncryptionContext).to.deep.equal(context) + expect(GrantTokens).to.equal(grantTokens) + expect(Buffer.from(CiphertextBlob).toString('utf8')).to.equal( + keyIdOtherRegion + ) + return { + Plaintext: new Uint8Array(suite.keyLengthBytes), + KeyId, + } + } + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.8 + //= type=test + //# To attempt to decrypt a particular encrypted data key + //# (structures.md#encrypted-data-key), OnDecrypt MUST call AWS KMS + //# Decrypt (https://docs.aws.amazon.com/kms/latest/APIReference/ + //# API_Decrypt.html) with the configured AWS KMS client. + const client: any = { decrypt } + + class TestAwsKmsMrkAwareSymmetricKeyring extends AwsKmsMrkAwareSymmetricKeyringClass( + Keyring as Newable> + ) {} + + const testKeyring = new TestAwsKmsMrkAwareSymmetricKeyring({ + client, + keyId: configuredKeyId, + grantTokens, + }) + + const edk = new EncryptedDataKey({ + providerId: 'aws-kms', + providerInfo: keyIdOtherRegion, + encryptedDataKey: Buffer.from(keyIdOtherRegion), + }) + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.8 + //= type=test + //# OnDecrypt MUST take decryption materials (structures.md#decryption- + //# materials) and a list of encrypted data keys + //# (structures.md#encrypted-data-key) as input. + const material = await testKeyring.onDecrypt( + new NodeDecryptionMaterial(suite, context), + [edk] + ) + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.8 + //= type=test + //# If the response does satisfies these requirements then OnDecrypt MUST + //# do the following with the response: + expect(material.hasUnencryptedDataKey).to.equal(true) + expect(material.keyringTrace).to.have.lengthOf(1) + const [traceDecrypt] = material.keyringTrace + expect(traceDecrypt.keyNamespace).to.equal('aws-kms') + expect(traceDecrypt.keyName).to.equal(configuredKeyId) + expect( + traceDecrypt.flags & KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY + ).to.equal(KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY) + expect( + traceDecrypt.flags & KeyringTraceFlag.WRAPPING_KEY_VERIFIED_ENC_CTX + ).to.equal(KeyringTraceFlag.WRAPPING_KEY_VERIFIED_ENC_CTX) + }) + + it('for configured non-MRK ARN', async () => { + const keyId = + 'arn:aws:kms:us-east-1:2222222222222:key/1234abcd-12ab-34cd-56ef-1234567890ab' + const context = { some: 'context' } + const grantTokens = ['grant'] + const suite = new NodeAlgorithmSuite( + AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16 + ) + + function decrypt({ + KeyId, + CiphertextBlob, + EncryptionContext, + GrantTokens, + }: any) { + expect(KeyId).to.equal(keyId) + expect(EncryptionContext).to.deep.equal(context) + expect(GrantTokens).to.equal(grantTokens) + return { + Plaintext: new Uint8Array(suite.keyLengthBytes), + KeyId: Buffer.from(CiphertextBlob as Uint8Array).toString('utf8'), + } + } + const client: any = { decrypt } + + class TestAwsKmsMrkAwareSymmetricKeyring extends AwsKmsMrkAwareSymmetricKeyringClass( + Keyring as Newable> + ) {} + + const testKeyring = new TestAwsKmsMrkAwareSymmetricKeyring({ + client, + keyId, + grantTokens, + }) + + const edk = new EncryptedDataKey({ + providerId: 'aws-kms', + providerInfo: keyId, + encryptedDataKey: Buffer.from(keyId), + }) + + const material = await testKeyring.onDecrypt( + new NodeDecryptionMaterial(suite, context), + [edk] + ) + + expect(material.hasUnencryptedDataKey).to.equal(true) + + expect(material.keyringTrace).to.have.lengthOf(1) + const [traceDecrypt] = material.keyringTrace + expect(traceDecrypt.keyNamespace).to.equal('aws-kms') + expect(traceDecrypt.keyName).to.equal(keyId) + expect( + traceDecrypt.flags & KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY + ).to.equal(KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY) + expect( + traceDecrypt.flags & KeyringTraceFlag.WRAPPING_KEY_VERIFIED_ENC_CTX + ).to.equal(KeyringTraceFlag.WRAPPING_KEY_VERIFIED_ENC_CTX) + }) + }) + + it('do not process any EDKs if an unencrypted data key exists.', async () => { + const keyId = + 'arn:aws:kms:us-east-1:2222222222222:key/1234abcd-12ab-34cd-56ef-1234567890ab' + const context = { some: 'context' } + const suite = new NodeAlgorithmSuite( + AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16 + ) + const client: any = {} + + class TestAwsKmsMrkAwareSymmetricKeyring extends AwsKmsMrkAwareSymmetricKeyringClass( + Keyring as Newable> + ) {} + + const testKeyring = new TestAwsKmsMrkAwareSymmetricKeyring({ + client, + keyId, + }) + + const seedMaterial = new NodeDecryptionMaterial( + suite, + context + ).setUnencryptedDataKey(new Uint8Array(suite.keyLengthBytes), { + keyNamespace: 'k', + keyName: 'k', + flags: KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY, + }) + + // The Provider info is malformed, + // if the keyring filters this, + // it should throw. + const edk = new EncryptedDataKey({ + providerId: 'aws-kms', + providerInfo: 'Not:an/arn', + encryptedDataKey: Buffer.from(keyId), + }) + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.8 + //= type=test + //# If the decryption materials (structures.md#decryption-materials) + //# already contained a valid plaintext data key OnDecrypt MUST + //# immediately return the unmodified decryption materials + //# (structures.md#decryption-materials). + const material = await testKeyring.onDecrypt(seedMaterial, [edk]) + expect(material === seedMaterial).to.equal(true) + }) + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.8 + //= type=test + //# For each encrypted data key in the filtered set, one at a time, the + //# OnDecrypt MUST attempt to decrypt the data key. + it('decrypt errors should not halt', async () => { + const mrk = + 'arn:aws:kms:us-west-1:123456789012:key/mrk-12345678123412341234123456789012' + + const context = { some: 'context' } + const grantTokens = ['grant'] + const suite = new NodeAlgorithmSuite( + AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16 + ) + + let edkCount = 0 + function decrypt({ + KeyId, + // CiphertextBlob, + EncryptionContext, + GrantTokens, + }: any) { + if (edkCount === 0) { + edkCount += 1 + throw new Error('failed to decrypt') + } + expect(EncryptionContext).to.deep.equal(context) + expect(GrantTokens).to.equal(grantTokens) + return { + Plaintext: new Uint8Array(suite.keyLengthBytes), + KeyId, + } + } + const client: any = { decrypt } + + class TestAwsKmsMrkAwareSymmetricKeyring extends AwsKmsMrkAwareSymmetricKeyringClass( + Keyring as Newable> + ) {} + + const testKeyring = new TestAwsKmsMrkAwareSymmetricKeyring({ + client, + keyId: mrk, + grantTokens, + }) + + const edk1 = new EncryptedDataKey({ + providerId: 'aws-kms', + providerInfo: mrk, + encryptedDataKey: Buffer.from(mrk), + }) + + const edk2 = new EncryptedDataKey({ + providerId: 'aws-kms', + providerInfo: mrk, + encryptedDataKey: Buffer.from(mrk), + }) + + const material = await testKeyring.onDecrypt( + new NodeDecryptionMaterial(suite, context), + [edk1, edk2] + ) + + expect(material.hasUnencryptedDataKey).to.equal(true) + expect(material.keyringTrace).to.have.lengthOf(1) + }) + + describe('unexpected KMS response', () => { + const usEastMrkArn = + 'arn:aws:kms:us-east-1:123456789012:key/mrk-12345678123412341234123456789012' + const usWestMrkArn = + 'arn:aws:kms:us-west-2:123456789012:key/mrk-12345678123412341234123456789012' + + const context = { some: 'context' } + const grantTokens = ['grant'] + const suite = new NodeAlgorithmSuite( + AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16 + ) + + it('keyId should should fail', async () => { + async function decrypt() { + return { + Plaintext: new Uint8Array(suite.keyLengthBytes), + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.8 + //= type=test + //# * The "KeyId" field in the response MUST equal the configured AWS + //# KMS key identifier. + KeyId: 'Not the Encrypted ARN', + } + } + const client: any = { decrypt } + + class TestAwsKmsMrkAwareSymmetricKeyring extends AwsKmsMrkAwareSymmetricKeyringClass( + Keyring as Newable> + ) {} + + const testKeyring = new TestAwsKmsMrkAwareSymmetricKeyring({ + client, + keyId: usEastMrkArn, + grantTokens, + }) + + const edk1 = new EncryptedDataKey({ + providerId: 'aws-kms', + providerInfo: usWestMrkArn, + encryptedDataKey: Buffer.from(usWestMrkArn), + }) + + await expect( + testKeyring.onDecrypt(new NodeDecryptionMaterial(suite, context), [ + edk1, + ]) + ).to.rejectedWith( + /KMS Decryption key does not match the requested key id./ + ) + }) + + it('plaintext length should fail', async () => { + function decrypt() { + return { + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.8 + //= type=test + //# * The length of the response's "Plaintext" MUST equal the key + //# derivation input length (algorithm-suites.md#key-derivation-input- + //# length) specified by the algorithm suite (algorithm-suites.md) + //# included in the input decryption materials + //# (structures.md#decryption-materials). + Plaintext: new Uint8Array(suite.keyLengthBytes - 5), + KeyId: usEastMrkArn, + } + } + const client: any = { decrypt } + + class TestAwsKmsMrkAwareSymmetricKeyring extends AwsKmsMrkAwareSymmetricKeyringClass( + Keyring as Newable> + ) {} + + const testKeyring = new TestAwsKmsMrkAwareSymmetricKeyring({ + client, + keyId: usEastMrkArn, + grantTokens, + }) + + const edk1 = new EncryptedDataKey({ + providerId: 'aws-kms', + providerInfo: usEastMrkArn, + encryptedDataKey: Buffer.from(usEastMrkArn), + }) + + await expect( + testKeyring.onDecrypt(new NodeDecryptionMaterial(suite, context), [ + edk1, + ]) + ).to.eventually.rejectedWith( + /Key length does not agree with the algorithm specification/ + ) + }) + }) + + it('does not attempt to decrypt non-matching EDKs', async () => { + const configuredKeyId = + 'arn:aws:kms:us-east-1:2222222222222:key/mrk-4321abcd12ab34cd56ef1234567890ab' + const otherKeyArn = + 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012' + const context = { some: 'context' } + const grantTokens = ['grant'] + const suite = new NodeAlgorithmSuite( + AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16 + ) + + let kmsCalled = false + function decrypt() { + kmsCalled = true + return { + Plaintext: new Uint8Array(suite.keyLengthBytes), + KeyId: configuredKeyId, + } + } + const client: any = { decrypt } + class TestAwsKmsMrkAwareSymmetricKeyring extends AwsKmsMrkAwareSymmetricKeyringClass( + Keyring as Newable> + ) {} + + const edk1 = new EncryptedDataKey({ + providerId: 'not aws kms edk', + providerInfo: configuredKeyId, + encryptedDataKey: Buffer.from(configuredKeyId), + }) + + const edk2 = new EncryptedDataKey({ + providerId: 'aws-kms', + providerInfo: otherKeyArn, + encryptedDataKey: Buffer.from(otherKeyArn), + }) + + const testKeyring = new TestAwsKmsMrkAwareSymmetricKeyring({ + client, + keyId: configuredKeyId, + grantTokens, + }) + + await expect( + testKeyring.onDecrypt(new NodeDecryptionMaterial(suite, context), [ + edk1, + edk2, + ]) + ).to.rejectedWith(Error, 'Unable to decrypt data key') + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.8 + //= type=test + //# The set of encrypted data keys MUST first be filtered to match this + //# keyring's configuration. + // + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.8 + //= type=test + //# * Its provider ID MUST exactly match the value "aws-kms". + expect(kmsCalled).to.equal(false) + }) + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.8 + //= type=test + //# * The the function AWS KMS MRK Match for Decrypt (aws-kms-mrk-match- + //# for-decrypt.md#implementation) called with the configured AWS KMS + //# key identifier and the provider info MUST return "true". + it('does not attempt to decrypt if configured with an MRK and EDKs that do not satisfy an MRK match', async () => { + const usEastMrkArn = + 'arn:aws:kms:us-east-1:123456789012:key/mrk-12345678123412341234123456789012' + const context = { some: 'context' } + const grantTokens = ['grant'] + const suite = new NodeAlgorithmSuite( + AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16 + ) + + let kmsCalled = false + function decrypt() { + kmsCalled = true + return { + Plaintext: new Uint8Array(suite.keyLengthBytes), + KeyId: usEastMrkArn, + } + } + const client: any = { decrypt } + class TestAwsKmsMrkAwareSymmetricKeyring extends AwsKmsMrkAwareSymmetricKeyringClass( + Keyring as Newable> + ) {} + + const otherKeyArn = + 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012' + const edk1 = new EncryptedDataKey({ + providerId: 'aws-kms', + providerInfo: otherKeyArn, + encryptedDataKey: Buffer.from(otherKeyArn), + }) + + const otherMrkArn = + 'arn:aws:kms:us-east-1:123456789012:key/mrk-00000000-0000-0000-0000-000000000000' + const edk2 = new EncryptedDataKey({ + providerId: 'aws-kms', + providerInfo: otherMrkArn, + encryptedDataKey: Buffer.from(otherMrkArn), + }) + + const otherPartitionMrkArn = + 'arn:not-aws:kms:us-east-1:123456789012:key/mrk-12345678123412341234123456789012' + const edk3 = new EncryptedDataKey({ + providerId: 'aws-kms', + providerInfo: otherPartitionMrkArn, + encryptedDataKey: Buffer.from(otherPartitionMrkArn), + }) + + const otherAccountMrkArn = + 'arn:aws:kms:us-east-1:098765432109:key/mrk-12345678123412341234123456789012' + const edk4 = new EncryptedDataKey({ + providerId: 'aws-kms', + providerInfo: otherAccountMrkArn, + encryptedDataKey: Buffer.from(otherAccountMrkArn), + }) + + const testKeyring = new TestAwsKmsMrkAwareSymmetricKeyring({ + client, + keyId: usEastMrkArn, + grantTokens, + }) + + await expect( + testKeyring.onDecrypt(new NodeDecryptionMaterial(suite, context), [ + edk1, + edk2, + edk3, + edk4, + ]) + ).to.rejectedWith(Error, 'Unable to decrypt data key') + expect(kmsCalled).to.equal(false) + }) + + it('halts and throws an error if encounters aws-kms EDK ProviderInfo with non-valid ARN', async () => { + const keyId = + 'arn:aws:kms:us-east-1:2222222222222:key/mrk-4321abcd12ab34cd56ef1234567890ab' + const context = { some: 'context' } + const grantTokens = ['grant'] + const suite = new NodeAlgorithmSuite( + AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16 + ) + + const client: any = {} + class TestAwsKmsMrkAwareSymmetricKeyring extends AwsKmsMrkAwareSymmetricKeyringClass( + Keyring as Newable> + ) {} + + const testKeyring = new TestAwsKmsMrkAwareSymmetricKeyring({ + client, + keyId, + grantTokens, + }) + + const invalidKeyId = 'Not:an/ARN' + const edk1 = new EncryptedDataKey({ + providerId: 'aws-kms', + providerInfo: invalidKeyId, + encryptedDataKey: Buffer.from(invalidKeyId), + }) + await expect( + testKeyring.onDecrypt(new NodeDecryptionMaterial(suite, context), [edk1]) + ).to.rejectedWith(Error, 'Malformed arn') + + const regionlessArn = + 'arn:aws:kms::2222222222222:key/mrk-4321abcd12ab34cd56ef1234567890ab' + const edk2 = new EncryptedDataKey({ + providerId: 'aws-kms', + providerInfo: regionlessArn, + encryptedDataKey: Buffer.from(regionlessArn), + }) + await expect( + testKeyring.onDecrypt(new NodeDecryptionMaterial(suite, context), [edk2]) + ).to.rejectedWith(Error, 'Malformed arn') + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.8 + //= type=test + //# * The provider info MUST be a valid AWS KMS ARN (aws-kms-key- + //# arn.md#a-valid-aws-kms-arn) with a resource type of "key" or + //# OnDecrypt MUST fail. + const aliasArn = 'arn:aws:kms:us-west-2:658956600833:alias/EncryptDecrypt' + const edk3 = new EncryptedDataKey({ + providerId: 'aws-kms', + providerInfo: aliasArn, + encryptedDataKey: Buffer.from(aliasArn), + }) + await expect( + testKeyring.onDecrypt(new NodeDecryptionMaterial(suite, context), [edk3]) + ).to.rejectedWith(Error, 'Unexpected EDK ProviderInfo for AWS KMS EDK') + }) + + describe('throws an error if does not successfully decrypt any EDK', () => { + it('because it encountered no EDKs to decrypt', async () => { + const keyId = + 'arn:aws:kms:us-east-1:2222222222222:key/mrk-4321abcd12ab34cd56ef1234567890ab' + const context = { some: 'context' } + const grantTokens = ['grant'] + const suite = new NodeAlgorithmSuite( + AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16 + ) + + const client: any = {} + class TestAwsKmsMrkAwareSymmetricKeyring extends AwsKmsMrkAwareSymmetricKeyringClass( + Keyring as Newable> + ) {} + + await expect( + new TestAwsKmsMrkAwareSymmetricKeyring({ + client, + keyId, + grantTokens, + }).onDecrypt(new NodeDecryptionMaterial(suite, context), []) + ).to.rejectedWith(Error, 'Unable to decrypt data key: No EDKs supplied.') + }) + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.8 + //= type=test + //# If this attempt + //# results in an error, then these errors MUST be collected. + it('and collects all errors encountered during decryption', async () => { + const usEastMrkArn = + 'arn:aws:kms:us-east-1:123456789012:key/mrk-12345678123412341234123456789012' + const usWestMrkArn = + 'arn:aws:kms:us-west-1:123456789012:key/mrk-12345678123412341234123456789012' + + const context = { some: 'context' } + const grantTokens = ['grant'] + const suite = new NodeAlgorithmSuite( + AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16 + ) + + let edkCount = 0 + function decrypt() { + edkCount += 1 + throw new Error(`Decrypt Error ${edkCount}`) + } + const client: any = { decrypt } + + class TestAwsKmsMrkAwareSymmetricKeyring extends AwsKmsMrkAwareSymmetricKeyringClass( + Keyring as Newable> + ) {} + + const testKeyring = new TestAwsKmsMrkAwareSymmetricKeyring({ + client, + keyId: usEastMrkArn, + grantTokens, + }) + + const edk1 = new EncryptedDataKey({ + providerId: 'aws-kms', + providerInfo: usEastMrkArn, + encryptedDataKey: Buffer.from(usEastMrkArn), + }) + + const edk2 = new EncryptedDataKey({ + providerId: 'aws-kms', + providerInfo: usWestMrkArn, + encryptedDataKey: Buffer.from(usWestMrkArn), + }) + const material = new NodeDecryptionMaterial(suite, context) + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.8 + //= type=test + //# If the response does not satisfies these requirements then an error + //# MUST be collected and the next encrypted data key in the filtered set + //# MUST be attempted. + // + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.8 + //= type=test + //# If OnDecrypt fails to successfully decrypt any encrypted data key + //# (structures.md#encrypted-data-key), then it MUST yield an error that + //# includes all the collected errors. + await expect( + testKeyring.onDecrypt(material, [edk1, edk2]) + ).to.rejectedWith( + Error, + /Unable to decrypt data key[\s\S]*Decrypt Error 1[\s\S]*Decrypt Error 2/ + ) + }) + }) +}) diff --git a/modules/kms-keyring/test/kms_mrk_keyring.onencrypt.test.ts b/modules/kms-keyring/test/kms_mrk_keyring.onencrypt.test.ts new file mode 100644 index 000000000..f30baa2d1 --- /dev/null +++ b/modules/kms-keyring/test/kms_mrk_keyring.onencrypt.test.ts @@ -0,0 +1,438 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +/* eslint-env mocha */ + +import chai from 'chai' +import chaiAsPromised from 'chai-as-promised' +import { AwsKmsMrkAwareSymmetricKeyringClass } from '../src/kms_mrk_keyring' +import { + NodeAlgorithmSuite, + AlgorithmSuiteIdentifier, + NodeEncryptionMaterial, + KeyringTraceFlag, + Keyring, + Newable, +} from '@aws-crypto/material-management' +chai.use(chaiAsPromised) +const { expect } = chai + +describe('AwsKmsMrkAwareSymmetricKeyring: _onEncrypt', () => { + it('Updates materials with data from KMS GenerateDataKey if input materials do not contain plaintext data key', async () => { + const keyId = + 'arn:aws:kms:us-east-1:2222222222222:key/mrk-4321abcd12ab34cd56ef1234567890ab' + const context = { some: 'context' } + const grantTokens = ['grant'] + const suite = new NodeAlgorithmSuite( + AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16 + ) + let generateCalled = false + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.7 + //= type=test + //# If the input encryption materials (structures.md#encryption- + //# materials) do not contain a plaintext data key OnEncrypt MUST attempt + //# to generate a new plaintext data key by calling AWS KMS + //# GenerateDataKey (https://docs.aws.amazon.com/kms/latest/APIReference/ + //# API_GenerateDataKey.html). + function generateDataKey({ + KeyId, + EncryptionContext, + GrantTokens, + NumberOfBytes, + }: any) { + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.7 + //= type=test + //# The keyring MUST call + //# AWS KMS GenerateDataKeys with a request constructed as follows: + expect(EncryptionContext).to.deep.equal(context) + expect(GrantTokens).to.equal(grantTokens) + expect(KeyId).to.equal(KeyId) + expect(NumberOfBytes).to.equal(suite.keyLengthBytes) + generateCalled = true + return { + Plaintext: new Uint8Array(suite.keyLengthBytes), + KeyId, + CiphertextBlob: new Uint8Array(5), + } + } + + const client: any = { generateDataKey } + class TestAwsKmsMrkAwareSymmetricKeyring extends AwsKmsMrkAwareSymmetricKeyringClass( + Keyring as Newable> + ) {} + + const testKeyring = new TestAwsKmsMrkAwareSymmetricKeyring({ + client, + keyId, + grantTokens, + }) + + const seedMaterial = new NodeEncryptionMaterial(suite, context) + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.7 + //= type=test + //# OnEncrypt MUST take encryption materials (structures.md#encryption- + //# materials) as input. + const material = await testKeyring.onEncrypt(seedMaterial) + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.7 + //= type=test + //# If verified, + //# OnEncrypt MUST do the following with the response from AWS KMS + //# GenerateDataKey (https://docs.aws.amazon.com/kms/latest/APIReference/ + //# API_GenerateDataKey.html): + expect(material.hasUnencryptedDataKey).to.equal(true) + + expect(material.encryptedDataKeys).to.have.lengthOf(1) + const [edkGenerate] = material.encryptedDataKeys + expect(edkGenerate.providerId).to.equal('aws-kms') + expect(edkGenerate.providerInfo).to.equal(keyId) + + expect(material.keyringTrace).to.have.lengthOf(2) + const [traceGenerate, traceEncrypt1] = material.keyringTrace + expect(traceGenerate.keyNamespace).to.equal('aws-kms') + expect(traceGenerate.keyName).to.equal(keyId) + expect( + traceGenerate.flags & KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY + ).to.equal(KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY) + expect( + traceGenerate.flags & KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY + ).to.equal(KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY) + expect( + traceGenerate.flags & KeyringTraceFlag.WRAPPING_KEY_SIGNED_ENC_CTX + ).to.equal(KeyringTraceFlag.WRAPPING_KEY_SIGNED_ENC_CTX) + + expect(traceEncrypt1.keyNamespace).to.equal('aws-kms') + expect(traceEncrypt1.keyName).to.equal(keyId) + expect( + traceEncrypt1.flags & KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY + ).to.equal(KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY) + expect( + traceEncrypt1.flags & KeyringTraceFlag.WRAPPING_KEY_SIGNED_ENC_CTX + ).to.equal(KeyringTraceFlag.WRAPPING_KEY_SIGNED_ENC_CTX) + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.7 + //= type=test + //# * OnEncrypt MUST output the modified encryption materials + //# (structures.md#encryption-materials) + expect(seedMaterial === material).to.equal(true) + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.7 + //= type=test + //# If the keyring calls AWS KMS GenerateDataKeys, it MUST use the + //# configured AWS KMS client to make the call. + expect(generateCalled).to.equal(true) + }) + + it('The generated unencryptedDataKey length must match the algorithm specification.', async () => { + const keyId = + 'arn:aws:kms:us-east-1:2222222222222:key/mrk-4321abcd12ab34cd56ef1234567890ab' + const encryptionContext = { some: 'context' } + const grantTokens = ['grant'] + const suite = new NodeAlgorithmSuite( + AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16 + ) + + function generateDataKey({ KeyId, EncryptionContext, GrantTokens }: any) { + expect(EncryptionContext).to.deep.equal(encryptionContext) + expect(GrantTokens).to.equal(grantTokens) + return { + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.7 + //= type=test + //# If the Generate Data Key call succeeds, OnEncrypt MUST verify that + //# the response "Plaintext" length matches the specification of the + //# algorithm suite (algorithm-suites.md)'s Key Derivation Input Length + //# field. + Plaintext: new Uint8Array(suite.keyLengthBytes - 5), + KeyId, + CiphertextBlob: new Uint8Array(5), + } + } + const client: any = { generateDataKey } + class TestAwsKmsMrkAwareSymmetricKeyring extends AwsKmsMrkAwareSymmetricKeyringClass( + Keyring as Newable> + ) {} + + const testKeyring = new TestAwsKmsMrkAwareSymmetricKeyring({ + client, + keyId, + grantTokens, + }) + + return expect( + testKeyring.onEncrypt( + new NodeEncryptionMaterial(suite, encryptionContext) + ) + ).to.rejectedWith( + Error, + 'Key length does not agree with the algorithm specification.' + ) + }) + + it('The KeyID returned by KMS GenerateDataKey must be a valid ARN', async () => { + const keyId = + 'arn:aws:kms:us-east-1:2222222222222:key/mrk-4321abcd12ab34cd56ef1234567890ab' + const encryptionContext = { some: 'context' } + const grantTokens = ['grant'] + const suite = new NodeAlgorithmSuite( + AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16 + ) + + function generateDataKey({ EncryptionContext, GrantTokens }: any) { + expect(EncryptionContext).to.deep.equal(encryptionContext) + expect(GrantTokens).to.equal(grantTokens) + return { + Plaintext: new Uint8Array(suite.keyLengthBytes), + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.7 + //= type=test + //# The Generate Data Key response's "KeyId" MUST be A valid AWS + //# KMS key ARN (aws-kms-key-arn.md#identifying-an-aws-kms-multi-region- + //# key). + KeyId: 'Not an arn', + CiphertextBlob: new Uint8Array(5), + } + } + const client: any = { generateDataKey } + class TestAwsKmsMrkAwareSymmetricKeyring extends AwsKmsMrkAwareSymmetricKeyringClass( + Keyring as Newable> + ) {} + + const testKeyring = new TestAwsKmsMrkAwareSymmetricKeyring({ + client, + keyId, + grantTokens, + }) + + return await expect( + testKeyring.onEncrypt( + new NodeEncryptionMaterial(suite, encryptionContext) + ) + ).to.rejectedWith(Error, 'Malformed arn') + }) + + it('fails if KMS GenerateDataKey fails', async () => { + const keyId = + 'arn:aws:kms:us-east-1:2222222222222:key/mrk-4321abcd12ab34cd56ef1234567890ab' + const encryptionContext = { some: 'context' } + const grantTokens = ['grant'] + const suite = new NodeAlgorithmSuite( + AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16 + ) + + function generateDataKey() { + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.7 + //= type=test + //# If the call to AWS KMS GenerateDataKey + //# (https://docs.aws.amazon.com/kms/latest/APIReference/ + //# API_GenerateDataKey.html) does not succeed, OnEncrypt MUST NOT modify + //# the encryption materials (structures.md#encryption-materials) and + //# MUST fail. + throw new Error('failed to generate data key') + } + const client: any = { generateDataKey } + class TestAwsKmsMrkAwareSymmetricKeyring extends AwsKmsMrkAwareSymmetricKeyringClass( + Keyring as Newable> + ) {} + + const testKeyring = new TestAwsKmsMrkAwareSymmetricKeyring({ + client, + keyId, + grantTokens, + }) + + const material = new NodeEncryptionMaterial(suite, encryptionContext) + + await expect(testKeyring.onEncrypt(material)).to.rejectedWith( + Error, + 'failed to generate data key' + ) + expect(material.hasValidKey()).to.equal(false) + expect(material.encryptedDataKeys.length).to.equal(0) + }) + + it('Updates materials with data from KMS Encrypt if input materials contain plaintext data key.', async () => { + const configuredKeyId = + 'arn:aws:kms:us-east-1:2222222222222:key/mrk-4321abcd12ab34cd56ef1234567890ab' + const encryptionContext = { some: 'context' } + const grantTokens = ['grant'] + const suite = new NodeAlgorithmSuite( + AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16 + ) + const udk = new Uint8Array(suite.keyLengthBytes).fill(2) + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.7 + //= type=test + //# The keyring MUST call AWS KMS Encrypt + //# (https://docs.aws.amazon.com/kms/latest/APIReference/ + //# API_Encrypt.html) using the configured AWS KMS client. + // + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.7 + //= type=test + //# The keyring + //# MUST AWS KMS Encrypt call with a request constructed as follows: + function encrypt({ + KeyId, + EncryptionContext, + GrantTokens, + Plaintext, + }: any) { + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.7 + //= type=test + //# Given a plaintext data key in the encryption materials + //# (structures.md#encryption-materials), OnEncrypt MUST attempt to + //# encrypt the plaintext data key using the configured AWS KMS key + //# identifier. + expect(KeyId).to.equal(configuredKeyId) + expect(Plaintext).to.deep.equal(udk) + expect(EncryptionContext).to.deep.equal(encryptionContext) + expect(GrantTokens).to.equal(grantTokens) + return { + KeyId, + CiphertextBlob: new Uint8Array(5), + grantTokens, + } + } + const client: any = { encrypt } + class TestAwsKmsMrkAwareSymmetricKeyring extends AwsKmsMrkAwareSymmetricKeyringClass( + Keyring as Newable> + ) {} + + const testKeyring = new TestAwsKmsMrkAwareSymmetricKeyring({ + client, + keyId: configuredKeyId, + grantTokens, + }) + + const seedMaterial = new NodeEncryptionMaterial( + suite, + encryptionContext + ).setUnencryptedDataKey(new Uint8Array(udk), { + keyName: 'keyName', + keyNamespace: 'keyNamespace', + flags: KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY, + }) + + const material = await testKeyring.onEncrypt(seedMaterial) + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.7 + //= type=test + //# If verified, OnEncrypt MUST do the following with the response from + //# AWS KMS Encrypt (https://docs.aws.amazon.com/kms/latest/APIReference/ + //# API_Encrypt.html): + expect(material.encryptedDataKeys).to.have.lengthOf(1) + const [kmsEDK] = material.encryptedDataKeys + expect(kmsEDK.providerId).to.equal('aws-kms') + expect(kmsEDK.providerInfo).to.equal(configuredKeyId) + + expect(material.keyringTrace).to.have.lengthOf(2) + const [, kmsTrace] = material.keyringTrace + expect(kmsTrace.keyNamespace).to.equal('aws-kms') + expect(kmsTrace.keyName).to.equal(configuredKeyId) + expect( + kmsTrace.flags & KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY + ).to.equal(KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY) + expect( + kmsTrace.flags & KeyringTraceFlag.WRAPPING_KEY_SIGNED_ENC_CTX + ).to.equal(KeyringTraceFlag.WRAPPING_KEY_SIGNED_ENC_CTX) + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.7 + //= type=test + //# If all Encrypt calls succeed, OnEncrypt MUST output the modified + //# encryption materials (structures.md#encryption-materials). + expect(material === seedMaterial).to.equal(true) + }) + + it('The KeyID returned by KMS Encrypt must be a valid ARN', async () => { + const keyId = + 'arn:aws:kms:us-east-1:2222222222222:key/mrk-4321abcd12ab34cd56ef1234567890ab' + const encryptionContext = { some: 'context' } + const grantTokens = ['grant'] + const suite = new NodeAlgorithmSuite( + AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16 + ) + + function encrypt({ EncryptionContext, GrantTokens }: any) { + expect(EncryptionContext).to.deep.equal(encryptionContext) + expect(GrantTokens).to.equal(grantTokens) + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.7 + //= type=test + //# If the Encrypt call succeeds The response's "KeyId" MUST be A valid + //# AWS KMS key ARN (aws-kms-key-arn.md#identifying-an-aws-kms-multi- + //# region-key). + return { + KeyId: 'Not an arn', + CiphertextBlob: new Uint8Array(5), + grantTokens, + } + } + const client: any = { encrypt } + class TestAwsKmsMrkAwareSymmetricKeyring extends AwsKmsMrkAwareSymmetricKeyringClass( + Keyring as Newable> + ) {} + + const testKeyring = new TestAwsKmsMrkAwareSymmetricKeyring({ + client, + keyId, + grantTokens, + }) + + const seedMaterial = new NodeEncryptionMaterial( + suite, + encryptionContext + ).setUnencryptedDataKey(new Uint8Array(suite.keyLengthBytes), { + keyName: 'keyName', + keyNamespace: 'keyNamespace', + flags: KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY, + }) + + return expect(testKeyring.onEncrypt(seedMaterial)).to.rejectedWith( + Error, + 'Malformed arn' + ) + }) + + it('fails if KMS Encrypt fails', async () => { + const keyId = + 'arn:aws:kms:us-east-1:2222222222222:key/mrk-4321abcd12ab34cd56ef1234567890ab' + const encryptionContext = { some: 'context' } + const grantTokens = ['grant'] + const suite = new NodeAlgorithmSuite( + AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16 + ) + + function encrypt() { + throw new Error('failed to encrypt') + } + const client: any = { encrypt } + class TestAwsKmsMrkAwareSymmetricKeyring extends AwsKmsMrkAwareSymmetricKeyringClass( + Keyring as Newable> + ) {} + + const testKeyring = new TestAwsKmsMrkAwareSymmetricKeyring({ + client, + keyId, + grantTokens, + }) + + const seedMaterial = new NodeEncryptionMaterial( + suite, + encryptionContext + ).setUnencryptedDataKey(new Uint8Array(suite.keyLengthBytes), { + keyName: 'keyName', + keyNamespace: 'keyNamespace', + flags: KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY, + }) + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.7 + //= type=test + //# If the call to AWS KMS Encrypt + //# (https://docs.aws.amazon.com/kms/latest/APIReference/ + //# API_Encrypt.html) does not succeed, OnEncrypt MUST fail. + await expect(testKeyring.onEncrypt(seedMaterial)).to.rejectedWith( + Error, + 'failed to encrypt' + ) + expect(seedMaterial.encryptedDataKeys.length).to.equal(0) + }) +}) diff --git a/modules/kms-keyring/test/kms_mrk_strict_multi_keyring.test.ts b/modules/kms-keyring/test/kms_mrk_strict_multi_keyring.test.ts new file mode 100644 index 000000000..e61df5bd7 --- /dev/null +++ b/modules/kms-keyring/test/kms_mrk_strict_multi_keyring.test.ts @@ -0,0 +1,246 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +/* eslint-env mocha */ + +import { expect } from 'chai' +import { getAwsKmsMrkAwareStrictMultiKeyringBuilder } from '../src/kms_mrk_strict_multi_keyring' +import { AwsKmsMrkAwareSymmetricKeyringClass } from '../src/kms_mrk_keyring' +import { + MultiKeyringNode, + Newable, + KeyringNode, +} from '@aws-crypto/material-management' + +describe('buildAwsKmsMrkAwareStrictMultiKeyringNode', () => { + class TestMrkAwareSymmetricKeyring extends AwsKmsMrkAwareSymmetricKeyringClass( + KeyringNode as Newable + ) {} + + const testBuilder = getAwsKmsMrkAwareStrictMultiKeyringBuilder( + TestMrkAwareSymmetricKeyring, + MultiKeyringNode, + (): any => {} + ) + + it('constructs expected child/generator keyrings', async () => { + const generatorKeyId = + 'arn:aws:kms:us-west-2:658956600833:alias/EncryptDecrypt' + const keyArn = + 'arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f' + const keyIds = [keyArn] + const grantTokens = ['grant', 'tokens'] + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.6 + //= type=test + //# The caller MUST provide: + const test = testBuilder({ + generatorKeyId, + keyIds, + clientProvider(region: string): any { + //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.6 + //= type=test + //# * The AWS KMS client that MUST be created by the regional client + //# supplier when called with the region part of the generator ARN or + //# a signal for the AWS SDK to select the default region. + // + //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.6 + //= type=test + //# * The AWS KMS client that MUST be created by the regional client + //# supplier when called with the region part of the AWS KMS key + //# identifier or a signal for the AWS SDK to select the default + //# region. + expect(region).to.equal('us-west-2') + return { config: { region } } + }, + grantTokens, + }) + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.6 + //= type=test + //# Then a Multi-Keyring (../multi-keyring.md#inputs) MUST be initialize + //# by using this generator keyring as the generator keyring (../multi- + //# keyring.md#generator-keyring) and this set of child keyrings as the + //# child keyrings (../multi-keyring.md#child-keyrings). + // + //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.6 + //= type=test + //# This Multi- + //# Keyring MUST be this functions output. + expect(test instanceof MultiKeyringNode).to.equal(true) + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.6 + //= type=test + //# If there is a generator input then the generator keyring MUST be a + //# AWS KMS MRK Aware Symmetric Keyring (aws-kms-mrk-aware-symmetric- + //# keyring.md) initialized with + expect(test.generator instanceof TestMrkAwareSymmetricKeyring).to.equal( + true + ) + //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.6 + //= type=test + //# If there is a set of child identifiers then a set of AWS KMS MRK + //# Aware Symmetric Keyring (aws-kms-mrk-aware-symmetric-keyring.md) MUST + //# be created for each AWS KMS key identifier by initialized each + //# keyring with + expect(test.children).to.have.lengthOf(1) + expect(test.children[0] instanceof TestMrkAwareSymmetricKeyring).to.equal( + true + ) + + const generator = test.generator as TestMrkAwareSymmetricKeyring + const child = test.children[0] as TestMrkAwareSymmetricKeyring + expect(!!generator.client).to.equal(true) + expect(!!child.client).to.equal(true) + // @ts-expect-error checking a private value + expect(generator.client.config.region).to.equal('us-west-2') + // @ts-expect-error checking a private value + expect(child.client.config.region).to.equal('us-west-2') + expect(generator.grantTokens).to.deep.equal(grantTokens) + expect(child.grantTokens).to.deep.equal(grantTokens) + }) + + it('returns instance of MultiKeyringNode', () => { + const generatorKeyId = + 'arn:aws:kms:us-west-2:658956600833:alias/EncryptDecrypt' + const test = testBuilder({ generatorKeyId, clientProvider: () => true }) + expect(test instanceof MultiKeyringNode).to.equal(true) + }) + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.6 + //= type=test + //# At least one non-null or non-empty string AWS + //# KMS key identifiers exists in the input this function MUST fail. + it('At least one non-null or non-empty string AWS KMS key identifiers exists in the input or this function MUST fail.', async () => { + const expectedErrorMessage = + 'Noop keyring is not allowed: Set a generatorKeyId or at least one keyId.' + // @ts-expect-error The function has required arguments + expect(() => testBuilder()).to.throw(Error, expectedErrorMessage) + expect(() => testBuilder({})).to.throw(Error, expectedErrorMessage) + expect(() => testBuilder({ keyIds: [] })).to.throw( + Error, + expectedErrorMessage + ) + }) + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.6 + //= type=test + //# If any of the AWS KMS key identifiers is null or an empty string this + //# function MUST fail. + it('If any of the AWS KMS key identifiers is null or an empty string this function MUST fail.', async () => { + const expectedErrorMessage = + 'Noop keyring is not allowed: Set a generatorKeyId or at least one keyId.' + const validKeyId = + 'arn:aws:kms:us-east-1:2222222222222:key/mrk-4321abcd12ab34cd56ef1234567890ab' + expect(() => + testBuilder({ + generatorKeyId: validKeyId, + keyIds: [''], + }) + ).to.throw(Error, expectedErrorMessage) + expect(() => + testBuilder({ + generatorKeyId: validKeyId, + keyIds: [validKeyId, ''], + }) + ).to.throw(Error, expectedErrorMessage) + expect(() => + testBuilder({ + generatorKeyId: validKeyId, + // @ts-expect-error undefined is not a string + keyIds: [undefined], + }) + ).to.throw(Error, expectedErrorMessage) + expect(() => + testBuilder({ + generatorKeyId: validKeyId, + // @ts-expect-error undefined is not a string + keyIds: [validKeyId, undefined], + }) + ).to.throw(Error, expectedErrorMessage) + expect(() => + testBuilder({ + generatorKeyId: validKeyId, + // @ts-expect-error null is not a string + keyIds: [null], + }) + ).to.throw(Error, expectedErrorMessage) + expect(() => + testBuilder({ + generatorKeyId: validKeyId, + // @ts-expect-error null is not a string + keyIds: [validKeyId, null], + }) + ).to.throw(Error, expectedErrorMessage) + }) + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.6 + //= type=test + //# All + //# AWS KMS identifiers are passed to Assert AWS KMS MRK are unique (aws- + //# kms-mrk-are-unique.md#Implementation) and the function MUST return + //# success otherwise this MUST fail. + it('related multi-region keys are not allowed.', async () => { + expect(() => + testBuilder({ + generatorKeyId: + 'arn:aws:kms:us-east-1:123456789012:key/mrk-12345678123412341234123456789012', + keyIds: ['mrk-12345678123412341234123456789012'], + }) + ).to.throw(Error, 'Related multi-Region keys:') + + expect(() => + testBuilder({ + keyIds: [ + 'mrk-12345678123412341234123456789012', + 'arn:aws:kms:us-east-1:123456789012:key/mrk-12345678123412341234123456789012', + ], + }) + ).to.throw(Error, 'Related multi-Region keys:') + }) + + it('Postcondition: If the configured clientProvider is not able to create a client for a defined generator key, throw an error.', async () => { + const generatorKeyId = + 'arn:aws:kms:us-east-1:2222222222222:key/mrk-4321abcd12ab34cd56ef1234567890ab' + const clientProvider: any = () => { + return false + } + expect(() => + testBuilder({ + clientProvider, + generatorKeyId, + }) + ).to.throw( + Error, + 'Configured clientProvider is unable to create a client for configured' + ) + }) + + it('Create an AWS KMS client with a default region.', async () => { + const generatorKeyId = 'alias/my-alias' + const testBuilder = getAwsKmsMrkAwareStrictMultiKeyringBuilder( + TestMrkAwareSymmetricKeyring, + MultiKeyringNode, + (region: string): any => { + // This is tested, because this is being passed to the builder. + //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.6 + //= type=test + //# If + //# a regional client supplier is not passed, then a default MUST be + //# created that takes a region string and generates a default AWS SDK + //# client for the given region. + // + //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.6 + //= type=test + //# NOTE: The AWS Encryption SDK SHOULD NOT attempt to evaluate its own + //# default region. + expect(region).to.equal('') + return {} + } + ) + + testBuilder({ + generatorKeyId, + }) + }) +}) diff --git a/modules/material-management-browser/src/index.ts b/modules/material-management-browser/src/index.ts index 7ad823bf5..40b3d794c 100644 --- a/modules/material-management-browser/src/index.ts +++ b/modules/material-management-browser/src/index.ts @@ -34,4 +34,5 @@ export { SignaturePolicy, MessageFormat, ClientOptions, + Newable, } from '@aws-crypto/material-management' diff --git a/modules/material-management-node/src/index.ts b/modules/material-management-node/src/index.ts index 07b2d9164..364fd72e0 100644 --- a/modules/material-management-node/src/index.ts +++ b/modules/material-management-node/src/index.ts @@ -37,4 +37,5 @@ export { SignaturePolicy, MessageFormat, ClientOptions, + Newable, } from '@aws-crypto/material-management' diff --git a/modules/material-management/src/index.ts b/modules/material-management/src/index.ts index 65b988030..ae9cf29bf 100644 --- a/modules/material-management/src/index.ts +++ b/modules/material-management/src/index.ts @@ -29,9 +29,13 @@ export { export { WebCryptoAlgorithmSuite } from './web_crypto_algorithms' export { NodeAlgorithmSuite } from './node_algorithms' -export { Keyring, KeyringNode, KeyringWebCrypto } from './keyring' +export * from './keyring' export { KeyringTrace, KeyringTraceFlag } from './keyring_trace' -export { MultiKeyringNode, MultiKeyringWebCrypto } from './multi_keyring' +export { + MultiKeyringNode, + MultiKeyringWebCrypto, + MultiKeyring, +} from './multi_keyring' export * from './materials_manager' export { diff --git a/modules/material-management/src/multi_keyring.ts b/modules/material-management/src/multi_keyring.ts index b8e92b148..ade2b1bb4 100644 --- a/modules/material-management/src/multi_keyring.ts +++ b/modules/material-management/src/multi_keyring.ts @@ -161,7 +161,8 @@ interface MultiKeyringInput { children?: Keyring[] } -interface MultiKeyring extends Keyring { +export interface MultiKeyring + extends Keyring { generator?: Keyring children: ReadonlyArray> } diff --git a/modules/material-management/src/types.ts b/modules/material-management/src/types.ts index 94775edd4..73b4d9b6b 100644 --- a/modules/material-management/src/types.ts +++ b/modules/material-management/src/types.ts @@ -111,3 +111,5 @@ export interface ClientOptions { commitmentPolicy: CommitmentPolicy maxEncryptedDataKeys: number | false } + +export type Newable = { new (...args: any[]): T } diff --git a/package.json b/package.json index 35a261d7c..4aba4776a 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "main": "index.js", "scripts": { "preinstall": "mkdir -p modules/integration-browser/build/main modules/integration-node/build/main; touch modules/integration-browser/build/main/cli.js modules/integration-node/build/main/cli.js", - "postinstall": "./util/bootstrap_tsconfig", + "postinstall": "./util/bootstrap_tsconfig && npm run build-version.ts", "clean": "npm run clear-build-cache && lerna clean", "clear-build-cache": "rimraf ./modules/*/build/*", "lint": "run-s lint-*", @@ -25,13 +25,13 @@ "build-node": "tsc -b tsconfig.json", "build-browser": "tsc -b tsconfig.module.json", "build": "run-s build-*", - "mocha": "mocha --require source-map-support/register --require ts-node/register --exclude 'modules/*-+(browser|backend)/test/*test.ts' modules/**/test/*test.ts", - "karma": "karma start karma.conf.js", + "mocha": "mocha --require source-map-support/register --require ts-node/register --exclude 'modules/*-+(browser|backend)/test/*test.ts' modules/**/test/*test.ts", + "karma": "NODE_OPTIONS=--max-old-space-size=4096 karma start karma.conf.js", "coverage-browser": "npm run karma && nyc report -t .karma_output --check-coverage --extension .ts", "coverage-node": "nyc --require ts-node/register --instrument --extension .ts --all --check-coverage -n 'modules/**/src/*.ts' -x 'modules/**/test/*.ts' -x 'modules/*-+(browser|backend)/**/*.ts' npm run mocha", "coverage": "run-s coverage-* && nyc merge .karma_output .nyc_output/browser.json && nyc report --extension .ts --check-coverage ", "report-coverage": "nyc report --extension .ts --check-coverage -r html", - "test": "npm run lint && npm run build && npm run coverage", + "test": "npm run build && npm run coverage", "integration-browser-decrypt": "npm run build; integration-browser decrypt -v $npm_package_config_localTestVectors --karma -c cpu", "integration-browser-encrypt": "npm run build; integration-browser encrypt -m $npm_package_config_encryptManifestList -k $npm_package_config_encryptKeyManifest -o $npm_package_config_decryptOracle --karma -c cpu", "browser-integration": "run-s integration-browser-*", @@ -45,10 +45,11 @@ "verdaccio-publish-browser-encrypt": "./util/npx_verdaccio @aws-crypto/integration-browser encrypt -m $npm_package_config_encryptManifestList -k $npm_package_config_encryptKeyManifest -o $npm_package_config_decryptOracle --karma -c cpu", "verdaccio-publish-node-decrypt": "./util/npx_verdaccio @aws-crypto/integration-node decrypt -v $npm_package_config_localTestVectors -c cpu", "verdaccio-publish-node-encrypt": "./util/npx_verdaccio @aws-crypto/integration-node encrypt -m $npm_package_config_encryptManifestList -k $npm_package_config_encryptKeyManifest -o $npm_package_config_decryptOracle -c cpu", - "test_conditions": "./util/test_conditions" + "test_conditions": "./aws-encryption-sdk-specification/util/test_conditions -s 'modules/**/src/*.ts' -t 'modules/**/test/*.ts' -s 'compliance_exceptions/*.ts'", + "duvet-report": "aws-encryption-sdk-specification/util/report.js modules/**/src/**/*.ts modules/**/test/**/*.ts compliance_exceptions/*.ts" }, "config": { - "localTestVectors": "aws-encryption-sdk-test-vectors/vectors/awses-decrypt/python-2.2.0.zip", + "localTestVectors": "aws-encryption-sdk-test-vectors/vectors/awses-decrypt/python-2.3.0-mrks.zip", "encryptManifestList": "https://raw.githubusercontent.com/awslabs/aws-crypto-tools-test-vector-framework/master/features/CANONICAL-GENERATED-MANIFESTS/0003-awses-message-encryption.v1.json", "encryptKeyManifest": "https://raw.githubusercontent.com/awslabs/aws-crypto-tools-test-vector-framework/master/features/CANONICAL-GENERATED-MANIFESTS/0002-keys.v1.json", "decryptOracle": "https://xi1mwx3ttb.execute-api.us-west-2.amazonaws.com/api/v0/decrypt" diff --git a/util/test_conditions b/util/test_conditions deleted file mode 100755 index 1e8b93e53..000000000 --- a/util/test_conditions +++ /dev/null @@ -1,95 +0,0 @@ -#!/usr/bin/env node -// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -/* This file is to help line up the formal conditions listed in source with tests. - * I look for `sourceGrep` and `testGrep` make make sure that the conditions found, - * are in both sets. - */ - -const { exec } = require('child_process') -const { promisify } = require('util') -const execAsync = promisify(exec) - -const ignoreIssueCount = parseInt(process.argv[2] || 0, 10) - -const sourceGrep = 'grep -E \'Precondition:|Postcondition(:|\\):)\' modules/**/src/*.ts' -const testGrep = 'grep -E \'Precondition:|Postcondition(:|\\):)\' modules/**/test/*.ts' - -Promise.all([ - execAsync(sourceGrep).then(clean), - execAsync(testGrep).then(clean) -]) - .then(([source, tests]) => { - let issues = 0 - const sourceSet = new Set(source) - const testSet = new Set(flatMapForTests(tests)) - - if (source.length > sourceSet.size) { - const duplicateSources = getDuplicates(source) - console.log('Duplicate source conditions', duplicateSources) - issues += duplicateSources.length - } - // A single test _may_ be multiple conditions. - const duplicateTests = getDuplicates(tests) - if (duplicateTests.length) { - console.log('Duplicate test conditions', duplicateTests) - issues += duplicateTests.length - } - - for (const sourceCondition of sourceSet) { - if (!testSet.has(sourceCondition)) { - console.log(`Missing test condition ${sourceCondition}`) - issues += 1 - } - } - - for (const testCondition of testSet) { - if (!sourceSet.has(testCondition)) { - console.log(`Update or change test condition ${testCondition}`) - issues += 1 - } - } - - if (issues) { - console.error(`Issue count found: ${issues}`) - } - - process.exit(Math.max(0, issues - ignoreIssueCount)) - }) - -const remove = /(\*\/)|(\/\*)|(it\([`'])|([`'], \(\) => \{)|([`'], async \(\) => \{)/g - -function clean ({ stdout }) { - return stdout.split('\n') - .map(l => l.replace(remove, '')) - // Do not try and clean up ' and things like that... - .map(l => l.split('.ts:').pop().trim()) -} - -function getDuplicates (arr) { - const found = new Set() - return arr.filter(item => { - if (!found.has(item)) { - found.add(item) - return false - } - return true - }) -} - -/* For a test that covers many conditions, ; can delimit the conditions - * e.g. - * Check for early return (Postcondition): Condition 1 ; Check for early return (Postcondition): Condition 2 - * This is primarily useful for Check for early return (Postcondition) - * as listed above because they can be easily check - * in a streaming context by increasing the buffer count by 1 byte - * at every iteration. - */ -function flatMapForTests (tests) { - const flatMapped = [] - tests.forEach(t => { - flatMapped.push(...t.split(';').map(t => t.trim())) - }) - return flatMapped -}