From 235c28bfc298f148763bf0c9d524a11812446f69 Mon Sep 17 00:00:00 2001 From: seebees Date: Thu, 12 Dec 2024 13:48:31 -0800 Subject: [PATCH 01/25] feat(branch-keystore): model AWS KMS configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(keystore): create class to model AWS KMS configuration for branch keystore * updated spec submodule to latest master * Update spec submodule to track master branch * feat(keystore): complete and test AWS KMS configuration class * chore: remove version file from branch-keystore-node module * chore: updated gitignore to ignore auto-generated version files in branch-keystore-node module * chore: removed changelog from branch-keystore-node module so that git can autogenerate it * added additional test for 100% coverage fix(CI): bump up lerna from 7.3.0 to 8.1.6 (#615) * bump up lerna * Revert "bump up lerna" This reverts commit 6b3853ea7e184f485c30d45c50c18ba2d1c7e1d9. * Revert "feat(branch-keystore): model AWS KMS configuration" This reverts commit fa8eabcb46290fdd1dbc99baf8ee1a3d2facdc25. * Reapply "feat(branch-keystore): model AWS KMS configuration" This reverts commit 96e8b3085530a67fa46fab653e173eb1db01a7e9. * bump lerna up from 7.3.0 to 8.1.6 * add dependencies to ensure proper build * npm audit fix * fix test compliance issues fix(branch-keystore): modify AWS KMS configuration to only support single region key compatibility for now (#608) * feat(branch-keystore): model AWS KMS configuration * feat(keystore): create class to model AWS KMS configuration for branch keystore * updated spec submodule to latest master * Update spec submodule to track master branch * feat(keystore): complete and test AWS KMS configuration class * chore: remove version file from branch-keystore-node module * chore: updated gitignore to ignore auto-generated version files in branch-keystore-node module * chore: removed changelog from branch-keystore-node module so that git can autogenerate it * added additional test for 100% coverage * made the fix and tested * remove duplicate compliance citations * specified compliance tests * fix compliance tests * fix duvet * remove duvet test annotations * add compliance tests for duvet * fix compliance tests for duvet * fix compliance tests for duvet * change lerna version * removed getParsedArn * separate kms config helpers from types * specified what's a 'bad arn' in tests * better error msg * no longer supressing errors from parseAwsKmsKeyArn * changed tests to assert for specific error messages * add a notice * sync lock file with package.json * consolidate helpers * compliance test citation * add additional flag methods to tell us config state * divide helper function tests and class method tests * add notice * Revert "change lerna version" This reverts commit a9ba112605c76295fb23cfda651f37eff9332e7b. * Update package-lock.json feat(cryptographic-materials-cache): add support for branch key materials (#596) * support branch key materials support branch key materials reinstall uuidv4 * reinstall uuidv4 within specific modules * install util package * uninstall uuidv4 package from code that may run in browser runtimes * generate uuid v4's using uuid package instead of uuidv4 * manually validate uuid v4's * install uuid package * remove uuidv4 regex validation * remove version lowercasing * add tests for v3 & v5 feat(hierarchical-keyring): Uuidv4 byte compression (#626) * wrote code and added tests from MPL * explain the ranges add plain interface (#595) feat(hierarchical-keyring): add branch keystore (#620) * chore: update package-lock.json (#1425) run `npm audit fix` * feat(branch-keystore): model AWS KMS configuration * feat(keystore): create class to model AWS KMS configuration for branch keystore * updated spec submodule to latest master * Update spec submodule to track master branch * feat(keystore): complete and test AWS KMS configuration class * chore: remove version file from branch-keystore-node module * chore: updated gitignore to ignore auto-generated version files in branch-keystore-node module * chore: removed changelog from branch-keystore-node module so that git can autogenerate it * added additional test for 100% coverage * fix(CI): bump up lerna from 7.3.0 to 8.1.6 (#615) * bump up lerna * Revert "bump up lerna" This reverts commit 6b3853ea7e184f485c30d45c50c18ba2d1c7e1d9. * Revert "feat(branch-keystore): model AWS KMS configuration" This reverts commit fa8eabcb46290fdd1dbc99baf8ee1a3d2facdc25. * Reapply "feat(branch-keystore): model AWS KMS configuration" This reverts commit 96e8b3085530a67fa46fab653e173eb1db01a7e9. * bump lerna up from 7.3.0 to 8.1.6 * add dependencies to ensure proper build * npm audit fix * fix test compliance issues * fix(branch-keystore): modify AWS KMS configuration to only support single region key compatibility for now (#608) * feat(branch-keystore): model AWS KMS configuration * feat(keystore): create class to model AWS KMS configuration for branch keystore * updated spec submodule to latest master * Update spec submodule to track master branch * feat(keystore): complete and test AWS KMS configuration class * chore: remove version file from branch-keystore-node module * chore: updated gitignore to ignore auto-generated version files in branch-keystore-node module * chore: removed changelog from branch-keystore-node module so that git can autogenerate it * added additional test for 100% coverage * made the fix and tested * remove duplicate compliance citations * specified compliance tests * fix compliance tests * fix duvet * remove duvet test annotations * add compliance tests for duvet * fix compliance tests for duvet * fix compliance tests for duvet * change lerna version * removed getParsedArn * separate kms config helpers from types * specified what's a 'bad arn' in tests * better error msg * no longer supressing errors from parseAwsKmsKeyArn * changed tests to assert for specific error messages * add a notice * sync lock file with package.json * consolidate helpers * compliance test citation * add additional flag methods to tell us config state * divide helper function tests and class method tests * add notice * Revert "change lerna version" This reverts commit a9ba112605c76295fb23cfda651f37eff9332e7b. * Update package-lock.json * Noop commit * wrote keystore * modify tests * modifying tests * add constructor tests * use material management module's branch key material class * more testing * create fixtures file to consolidate all test constants * rename * more tests and duvet * add copyright notice * fix test * fix test * change interface name * change param type to interface * change method signature * change return types because this is a node package * indicate integration tests * add mock network calls todo * better error message for getBranchKeyItem helper * more concise * leave grant tokens empty * modify mock todo * consolidate constants into one file * add notice * remove tests involving multi region keys * moved non-resource info out of fixtures * reinstall dependencies * sync lockfile after rebase * assume SRK * changes * rename keystore interface --------- Co-authored-by: seebees feat(hierarchical-keyring): KDF in counter mode with pseudorandom function (#609) * kdf * sync lockfile * nonce is required * improve Uint32ToSeq function * use pre-existing uint32 to big endian byte array * rename kdf.ts to kdfctr.ts * change macLengthBytes to h as in SP800-108 * change # iterations formula * removed dead code * made nonce optional and added a test * simplify calculation * remove repeated precondition Since kdfCtrMode will be exported from this module, its preconditions are also rawDerive's preconditions * better spacing * match test file name with src file name * optional nonce test * explain why offset is not a param * correct # iters * modify preconditions * chore: update package-lock.json (#1425) run `npm audit fix` * chore(CI): Add CodeBuild GHA Runner (#603) * feat(branch-keystore): model AWS KMS configuration * feat(keystore): create class to model AWS KMS configuration for branch keystore * updated spec submodule to latest master * Update spec submodule to track master branch * feat(keystore): complete and test AWS KMS configuration class * chore: remove version file from branch-keystore-node module * chore: updated gitignore to ignore auto-generated version files in branch-keystore-node module * chore: removed changelog from branch-keystore-node module so that git can autogenerate it * added additional test for 100% coverage * fix(CI): bump up lerna from 7.3.0 to 8.1.6 (#615) * bump up lerna * Revert "bump up lerna" This reverts commit 6b3853ea7e184f485c30d45c50c18ba2d1c7e1d9. * Revert "feat(branch-keystore): model AWS KMS configuration" This reverts commit fa8eabcb46290fdd1dbc99baf8ee1a3d2facdc25. * Reapply "feat(branch-keystore): model AWS KMS configuration" This reverts commit 96e8b3085530a67fa46fab653e173eb1db01a7e9. * bump lerna up from 7.3.0 to 8.1.6 * add dependencies to ensure proper build * npm audit fix * fix test compliance issues * fix(branch-keystore): modify AWS KMS configuration to only support single region key compatibility for now (#608) * feat(branch-keystore): model AWS KMS configuration * feat(keystore): create class to model AWS KMS configuration for branch keystore * updated spec submodule to latest master * Update spec submodule to track master branch * feat(keystore): complete and test AWS KMS configuration class * chore: remove version file from branch-keystore-node module * chore: updated gitignore to ignore auto-generated version files in branch-keystore-node module * chore: removed changelog from branch-keystore-node module so that git can autogenerate it * added additional test for 100% coverage * made the fix and tested * remove duplicate compliance citations * specified compliance tests * fix compliance tests * fix duvet * remove duvet test annotations * add compliance tests for duvet * fix compliance tests for duvet * fix compliance tests for duvet * change lerna version * removed getParsedArn * separate kms config helpers from types * specified what's a 'bad arn' in tests * better error msg * no longer supressing errors from parseAwsKmsKeyArn * changed tests to assert for specific error messages * add a notice * sync lock file with package.json * consolidate helpers * compliance test citation * add additional flag methods to tell us config state * divide helper function tests and class method tests * add notice * Revert "change lerna version" This reverts commit a9ba112605c76295fb23cfda651f37eff9332e7b. * Update package-lock.json * feat(cryptographic-materials-cache): add support for branch key materials (#596) * support branch key materials support branch key materials reinstall uuidv4 * reinstall uuidv4 within specific modules * install util package * uninstall uuidv4 package from code that may run in browser runtimes * generate uuid v4's using uuid package instead of uuidv4 * manually validate uuid v4's * install uuid package * remove uuidv4 regex validation * remove version lowercasing * add tests for v3 & v5 * kdf * sync lockfile * nonce is required * improve Uint32ToSeq function * use pre-existing uint32 to big endian byte array * rename kdf.ts to kdfctr.ts * change macLengthBytes to h as in SP800-108 * change # iterations formula * removed dead code * made nonce optional and added a test * simplify calculation * remove repeated precondition Since kdfCtrMode will be exported from this module, its preconditions are also rawDerive's preconditions * better spacing * match test file name with src file name * optional nonce test * explain why offset is not a param * correct # iters * modify preconditions * sync lock file * chore: update package-lock.json (#1425) run `npm audit fix` * feat(branch-keystore): model AWS KMS configuration * feat(keystore): create class to model AWS KMS configuration for branch keystore * updated spec submodule to latest master * Update spec submodule to track master branch * feat(keystore): complete and test AWS KMS configuration class * chore: remove version file from branch-keystore-node module * chore: updated gitignore to ignore auto-generated version files in branch-keystore-node module * chore: removed changelog from branch-keystore-node module so that git can autogenerate it * added additional test for 100% coverage * fix(CI): bump up lerna from 7.3.0 to 8.1.6 (#615) * bump up lerna * Revert "bump up lerna" This reverts commit 6b3853ea7e184f485c30d45c50c18ba2d1c7e1d9. * Revert "feat(branch-keystore): model AWS KMS configuration" This reverts commit fa8eabcb46290fdd1dbc99baf8ee1a3d2facdc25. * Reapply "feat(branch-keystore): model AWS KMS configuration" This reverts commit 96e8b3085530a67fa46fab653e173eb1db01a7e9. * bump lerna up from 7.3.0 to 8.1.6 * add dependencies to ensure proper build * npm audit fix * fix test compliance issues * fix(branch-keystore): modify AWS KMS configuration to only support single region key compatibility for now (#608) * feat(branch-keystore): model AWS KMS configuration * feat(keystore): create class to model AWS KMS configuration for branch keystore * updated spec submodule to latest master * Update spec submodule to track master branch * feat(keystore): complete and test AWS KMS configuration class * chore: remove version file from branch-keystore-node module * chore: updated gitignore to ignore auto-generated version files in branch-keystore-node module * chore: removed changelog from branch-keystore-node module so that git can autogenerate it * added additional test for 100% coverage * made the fix and tested * remove duplicate compliance citations * specified compliance tests * fix compliance tests * fix duvet * remove duvet test annotations * add compliance tests for duvet * fix compliance tests for duvet * fix compliance tests for duvet * change lerna version * removed getParsedArn * separate kms config helpers from types * specified what's a 'bad arn' in tests * better error msg * no longer supressing errors from parseAwsKmsKeyArn * changed tests to assert for specific error messages * add a notice * sync lock file with package.json * consolidate helpers * compliance test citation * add additional flag methods to tell us config state * divide helper function tests and class method tests * add notice * Revert "change lerna version" This reverts commit a9ba112605c76295fb23cfda651f37eff9332e7b. * Update package-lock.json * feat(cryptographic-materials-cache): add support for branch key materials (#596) * support branch key materials support branch key materials reinstall uuidv4 * reinstall uuidv4 within specific modules * install util package * uninstall uuidv4 package from code that may run in browser runtimes * generate uuid v4's using uuid package instead of uuidv4 * manually validate uuid v4's * install uuid package * remove uuidv4 regex validation * remove version lowercasing * add tests for v3 & v5 * kdf * sync lockfile * nonce is required * improve Uint32ToSeq function * use pre-existing uint32 to big endian byte array * rename kdf.ts to kdfctr.ts * change macLengthBytes to h as in SP800-108 * change # iterations formula * removed dead code * made nonce optional and added a test * simplify calculation * remove repeated precondition Since kdfCtrMode will be exported from this module, its preconditions are also rawDerive's preconditions * better spacing * match test file name with src file name * optional nonce test * explain why offset is not a param * correct # iters * modify preconditions * sync lock file * kdf * sync lockfile * nonce is required * improve Uint32ToSeq function * use pre-existing uint32 to big endian byte array * rename kdf.ts to kdfctr.ts * made nonce optional and added a test * remove repeated precondition Since kdfCtrMode will be exported from this module, its preconditions are also rawDerive's preconditions * match test file name with src file name * sync lock file after rebase * chain the calls * chore: update package-lock.json (#1425) run `npm audit fix` * feat(branch-keystore): model AWS KMS configuration * feat(keystore): create class to model AWS KMS configuration for branch keystore * updated spec submodule to latest master * Update spec submodule to track master branch * feat(keystore): complete and test AWS KMS configuration class * chore: remove version file from branch-keystore-node module * chore: updated gitignore to ignore auto-generated version files in branch-keystore-node module * chore: removed changelog from branch-keystore-node module so that git can autogenerate it * added additional test for 100% coverage * fix(CI): bump up lerna from 7.3.0 to 8.1.6 (#615) * bump up lerna * Revert "bump up lerna" This reverts commit 6b3853ea7e184f485c30d45c50c18ba2d1c7e1d9. * Revert "feat(branch-keystore): model AWS KMS configuration" This reverts commit fa8eabcb46290fdd1dbc99baf8ee1a3d2facdc25. * Reapply "feat(branch-keystore): model AWS KMS configuration" This reverts commit 96e8b3085530a67fa46fab653e173eb1db01a7e9. * bump lerna up from 7.3.0 to 8.1.6 * add dependencies to ensure proper build * npm audit fix * fix test compliance issues * fix(branch-keystore): modify AWS KMS configuration to only support single region key compatibility for now (#608) * feat(branch-keystore): model AWS KMS configuration * feat(keystore): create class to model AWS KMS configuration for branch keystore * updated spec submodule to latest master * Update spec submodule to track master branch * feat(keystore): complete and test AWS KMS configuration class * chore: remove version file from branch-keystore-node module * chore: updated gitignore to ignore auto-generated version files in branch-keystore-node module * chore: removed changelog from branch-keystore-node module so that git can autogenerate it * added additional test for 100% coverage * made the fix and tested * remove duplicate compliance citations * specified compliance tests * fix compliance tests * fix duvet * remove duvet test annotations * add compliance tests for duvet * fix compliance tests for duvet * fix compliance tests for duvet * change lerna version * removed getParsedArn * separate kms config helpers from types * specified what's a 'bad arn' in tests * better error msg * no longer supressing errors from parseAwsKmsKeyArn * changed tests to assert for specific error messages * add a notice * sync lock file with package.json * consolidate helpers * compliance test citation * add additional flag methods to tell us config state * divide helper function tests and class method tests * add notice * Revert "change lerna version" This reverts commit a9ba112605c76295fb23cfda651f37eff9332e7b. * Update package-lock.json * feat(cryptographic-materials-cache): add support for branch key materials (#596) * support branch key materials support branch key materials reinstall uuidv4 * reinstall uuidv4 within specific modules * install util package * uninstall uuidv4 package from code that may run in browser runtimes * generate uuid v4's using uuid package instead of uuidv4 * manually validate uuid v4's * install uuid package * remove uuidv4 regex validation * remove version lowercasing * add tests for v3 & v5 * kdf * sync lockfile * nonce is required * improve Uint32ToSeq function * use pre-existing uint32 to big endian byte array * rename kdf.ts to kdfctr.ts * change macLengthBytes to h as in SP800-108 * change # iterations formula * removed dead code * made nonce optional and added a test * simplify calculation * remove repeated precondition Since kdfCtrMode will be exported from this module, its preconditions are also rawDerive's preconditions * better spacing * match test file name with src file name * optional nonce test * explain why offset is not a param * correct # iters * modify preconditions * sync lock file * kdf * sync lockfile * nonce is required * improve Uint32ToSeq function * use pre-existing uint32 to big endian byte array * rename kdf.ts to kdfctr.ts * made nonce optional and added a test * remove repeated precondition Since kdfCtrMode will be exported from this module, its preconditions are also rawDerive's preconditions * match test file name with src file name * sync lock file after rebase * chore: update package-lock.json (#1425) run `npm audit fix` * feat(branch-keystore): model AWS KMS configuration * feat(keystore): create class to model AWS KMS configuration for branch keystore * updated spec submodule to latest master * Update spec submodule to track master branch * feat(keystore): complete and test AWS KMS configuration class * chore: remove version file from branch-keystore-node module * chore: updated gitignore to ignore auto-generated version files in branch-keystore-node module * chore: removed changelog from branch-keystore-node module so that git can autogenerate it * added additional test for 100% coverage * fix(CI): bump up lerna from 7.3.0 to 8.1.6 (#615) * bump up lerna * Revert "bump up lerna" This reverts commit 6b3853ea7e184f485c30d45c50c18ba2d1c7e1d9. * Revert "feat(branch-keystore): model AWS KMS configuration" This reverts commit fa8eabcb46290fdd1dbc99baf8ee1a3d2facdc25. * Reapply "feat(branch-keystore): model AWS KMS configuration" This reverts commit 96e8b3085530a67fa46fab653e173eb1db01a7e9. * bump lerna up from 7.3.0 to 8.1.6 * add dependencies to ensure proper build * npm audit fix * fix test compliance issues * fix(branch-keystore): modify AWS KMS configuration to only support single region key compatibility for now (#608) * feat(branch-keystore): model AWS KMS configuration * feat(keystore): create class to model AWS KMS configuration for branch keystore * updated spec submodule to latest master * Update spec submodule to track master branch * feat(keystore): complete and test AWS KMS configuration class * chore: remove version file from branch-keystore-node module * chore: updated gitignore to ignore auto-generated version files in branch-keystore-node module * chore: removed changelog from branch-keystore-node module so that git can autogenerate it * added additional test for 100% coverage * made the fix and tested * remove duplicate compliance citations * specified compliance tests * fix compliance tests * fix duvet * remove duvet test annotations * add compliance tests for duvet * fix compliance tests for duvet * fix compliance tests for duvet * change lerna version * removed getParsedArn * separate kms config helpers from types * specified what's a 'bad arn' in tests * better error msg * no longer supressing errors from parseAwsKmsKeyArn * changed tests to assert for specific error messages * add a notice * sync lock file with package.json * consolidate helpers * compliance test citation * add additional flag methods to tell us config state * divide helper function tests and class method tests * add notice * Revert "change lerna version" This reverts commit a9ba112605c76295fb23cfda651f37eff9332e7b. * Update package-lock.json * feat(cryptographic-materials-cache): add support for branch key materials (#596) * support branch key materials support branch key materials reinstall uuidv4 * reinstall uuidv4 within specific modules * install util package * uninstall uuidv4 package from code that may run in browser runtimes * generate uuid v4's using uuid package instead of uuidv4 * manually validate uuid v4's * install uuid package * remove uuidv4 regex validation * remove version lowercasing * add tests for v3 & v5 * kdf * sync lockfile * nonce is required * improve Uint32ToSeq function * use pre-existing uint32 to big endian byte array * rename kdf.ts to kdfctr.ts * made nonce optional and added a test * remove repeated precondition Since kdfCtrMode will be exported from this module, its preconditions are also rawDerive's preconditions * match test file name with src file name * sync lock file * kdf * sync lockfile * nonce is required * improve Uint32ToSeq function * use pre-existing uint32 to big endian byte array * rename kdf.ts to kdfctr.ts * made nonce optional and added a test * remove repeated precondition Since kdfCtrMode will be exported from this module, its preconditions are also rawDerive's preconditions * match test file name with src file name * chain the calls * sync lockfile after rebase --------- Co-authored-by: seebees Co-authored-by: José Corella <39066999+josecorella@users.noreply.github.com> fix(keystore): Fix kms config (#627) * change kms config remove flag methods remove assertValidNotAliasArn function * less arn parsing util exports needed type checks to class constructors and methods (#637) * type checks to class constructors and methods * modify grant token initialization feat(kms-keyring-node): add AWS KMS Hierarchical keyring (#632) * hkr * remove timeouts * fix test timeout issues * add an additional verification check * set up mocking * document mock mechanism * hkr * remove timeouts * fix test timeout issues * add an additional verification check * set up mocking * document mock mechanism * added runtime type checks to constructor * try fixing dep errors * fixes * add notice * renaming and modified preconditions Number attributes like TTL and max cache size can only be stored with precision if they are under JavaScript's Number.MAX_SAFE_INTEGER. In the MPL, TTL can be a non-negative signed 64-bit integer. However, JavaScript numbers cannot safely store integers beyond Number.MAX_SAFE_INTEGER. Thus, we will cap TTL in seconds such that TTL in ms is <= Number.MAX_SAFE_INTEGER. TTL could be a JS BigInt type but this would require casting back to a number in order to configure the CMC (which only deals with number types not BigInt), which leads to a lossy conversion. This same reasoning is applied to max cache size. Preconditions and tests for these preconditions are updated. * change in wrapping AAD logic * Update modules/kms-keyring-node/src/constants.ts add comment about encrypted key length in the ciphertext Co-authored-by: Rishav karanjit * update constants change name of the kdf digest algorithm constant to specify sha256. Increases readability * update constants change provider id constant name to specify hierarchy --------- Co-authored-by: Rishav karanjit hkr remove timeouts fix test timeout issues add an additional verification check set up mocking document mock mechanism added runtime type checks to constructor hkr remove timeouts fix test timeout issues add an additional verification check set up mocking document mock mechanism fixes add notice renaming and modified preconditions Number attributes like TTL and max cache size can only be stored with precision if they are under JavaScript's Number.MAX_SAFE_INTEGER. In the MPL, TTL can be a non-negative signed 64-bit integer. However, JavaScript numbers cannot safely store integers beyond Number.MAX_SAFE_INTEGER. Thus, we will cap TTL in seconds such that TTL in ms is <= Number.MAX_SAFE_INTEGER. TTL could be a JS BigInt type but this would require casting back to a number in order to configure the CMC (which only deals with number types not BigInt), which leads to a lossy conversion. This same reasoning is applied to max cache size. Preconditions and tests for these preconditions are updated. change in wrapping AAD logic update constants change name of the kdf digest algorithm constant to specify sha256. Increases readability update constants change provider id constant name to specify hierarchy create example file wrote code add notice and sync lockfile fix lint issue document the example more examples change class name add a comment about example branch key id supplier impl demo code and readme comment the demo code update duvet anotations first cut add storage Updates to H-Keyring Update tests all tests pass and all features “work” adding duvet anotations More duvet updates update the anotations Add discovery More citations Add expiration Update the spec version Updates add uuid to serialize update package lock update this one --- .gitignore | 1 + .gitmodules | 1 + aws-encryption-sdk-specification | 2 +- modules/branch-keystore-node/.eslintrc.js | 12 + modules/branch-keystore-node/.gitignore | 3 + modules/branch-keystore-node/LICENSE | 202 +++++ modules/branch-keystore-node/NOTICE | 2 + modules/branch-keystore-node/README.md | 31 + modules/branch-keystore-node/package.json | 34 + .../src/branch_keystore.ts | 693 ++++++++++++++++++ .../src/branch_keystore_helpers.ts | 361 +++++++++ .../src/branch_keystore_structures.ts | 25 + modules/branch-keystore-node/src/constants.ts | 27 + .../src/dynamodb_key_storage.ts | 217 ++++++ modules/branch-keystore-node/src/index.ts | 5 + .../branch-keystore-node/src/kms_config.ts | 228 ++++++ modules/branch-keystore-node/src/types.ts | 296 ++++++++ .../test/branch_keystore.test.ts | 601 +++++++++++++++ .../test/branch_keystore_helpers.test.ts | 686 +++++++++++++++++ modules/branch-keystore-node/test/fixtures.ts | 101 +++ .../test/kms_config.test.ts | 239 ++++++ modules/branch-keystore-node/tsconfig.json | 13 + .../branch-keystore-node/tsconfig.module.json | 12 + modules/cache-material/package.json | 3 +- .../src/cryptographic_materials_cache.ts | 20 + ...get_local_cryptographic_materials_cache.ts | 47 +- ...ocal_cryptographic_materials_cache.test.ts | 90 +++ .../src/caching_materials_manager_node.ts | 4 + modules/client-node/package.json | 2 + modules/client-node/src/index.ts | 2 + modules/client-node/tsconfig.json | 4 +- modules/example-node/hkr-demo/README.md | 104 +++ modules/example-node/hkr-demo/hkr.ts | 132 ++++ .../hkr-demo/hkr_vs_regular.demo.ts | 52 ++ modules/example-node/hkr-demo/interop.demo.ts | 103 +++ .../hkr-demo/multi_tenant.demo.ts | 174 +++++ .../kms-hierarchical-keyring/caching_cmm.ts | 170 +++++ .../disable_commitment.ts | 93 +++ .../kms-hierarchical-keyring/multi_keyring.ts | 133 ++++ .../kms-hierarchical-keyring/multi_tenancy.ts | 217 ++++++ .../src/kms-hierarchical-keyring/simple.ts | 125 ++++ .../src/kms-hierarchical-keyring/stream.ts | 126 ++++ modules/example-node/test/index.test.ts | 54 +- modules/kdf-ctr-mode-node/.eslintrc.js | 12 + modules/kdf-ctr-mode-node/CHANGELOG.md | 59 ++ modules/kdf-ctr-mode-node/LICENSE | 202 +++++ modules/kdf-ctr-mode-node/NOTICE | 5 + modules/kdf-ctr-mode-node/README.md | 36 + modules/kdf-ctr-mode-node/package.json | 42 ++ modules/kdf-ctr-mode-node/src/index.ts | 4 + modules/kdf-ctr-mode-node/src/kdfctr.ts | 157 ++++ modules/kdf-ctr-mode-node/test/kdfctr.test.ts | 311 ++++++++ modules/kdf-ctr-mode-node/test/testvectors.ts | 218 ++++++ modules/kdf-ctr-mode-node/tsconfig.json | 10 + .../kdf-ctr-mode-node/tsconfig.module.json | 12 + modules/kms-keyring-node/package.json | 6 + modules/kms-keyring-node/src/constants.ts | 29 + modules/kms-keyring-node/src/index.ts | 1 + .../kms-keyring-node/src/kms_hkeyring_node.ts | 487 ++++++++++++ .../src/kms_hkeyring_node_helpers.ts | 615 ++++++++++++++++ modules/kms-keyring-node/test/fixtures.ts | 56 ++ .../kms_hkeyring_node.constructor.test.ts | 292 ++++++++ .../test/kms_hkeyring_node.edk-order.test.ts | 323 ++++++++ .../test/kms_hkeyring_node.helpers.test.ts | 269 +++++++ .../test/kms_hkeyring_node.ondecrypt.test.ts | 658 +++++++++++++++++ .../test/kms_hkeyring_node.onencrypt.test.ts | 416 +++++++++++ .../test/kms_hkeyring_node.test.ts | 397 ++++++++++ modules/kms-keyring-node/tsconfig.json | 8 +- .../kms-keyring/src/branch_key_id_supplier.ts | 23 + modules/kms-keyring/src/index.ts | 2 + .../test/branch_key_id_supplier.test.ts | 29 + modules/material-management/package.json | 3 +- .../src/cryptographic_material.ts | 105 +++ modules/material-management/src/index.ts | 2 + modules/material-management/src/types.ts | 3 + .../test/cryptographic_material.test.ts | 188 ++++- modules/serialize/package.json | 3 +- modules/serialize/src/index.ts | 1 + modules/serialize/src/uuidv4_factory.ts | 68 ++ modules/serialize/test/uuidv4_factory.test.ts | 114 +++ package.json | 6 +- wallaby.conf.js | 36 +- 82 files changed, 10625 insertions(+), 30 deletions(-) create mode 100644 modules/branch-keystore-node/.eslintrc.js create mode 100644 modules/branch-keystore-node/.gitignore create mode 100644 modules/branch-keystore-node/LICENSE create mode 100644 modules/branch-keystore-node/NOTICE create mode 100644 modules/branch-keystore-node/README.md create mode 100644 modules/branch-keystore-node/package.json create mode 100644 modules/branch-keystore-node/src/branch_keystore.ts create mode 100644 modules/branch-keystore-node/src/branch_keystore_helpers.ts create mode 100644 modules/branch-keystore-node/src/branch_keystore_structures.ts create mode 100644 modules/branch-keystore-node/src/constants.ts create mode 100644 modules/branch-keystore-node/src/dynamodb_key_storage.ts create mode 100644 modules/branch-keystore-node/src/index.ts create mode 100644 modules/branch-keystore-node/src/kms_config.ts create mode 100644 modules/branch-keystore-node/src/types.ts create mode 100644 modules/branch-keystore-node/test/branch_keystore.test.ts create mode 100644 modules/branch-keystore-node/test/branch_keystore_helpers.test.ts create mode 100644 modules/branch-keystore-node/test/fixtures.ts create mode 100644 modules/branch-keystore-node/test/kms_config.test.ts create mode 100644 modules/branch-keystore-node/tsconfig.json create mode 100644 modules/branch-keystore-node/tsconfig.module.json create mode 100644 modules/example-node/hkr-demo/README.md create mode 100644 modules/example-node/hkr-demo/hkr.ts create mode 100644 modules/example-node/hkr-demo/hkr_vs_regular.demo.ts create mode 100644 modules/example-node/hkr-demo/interop.demo.ts create mode 100644 modules/example-node/hkr-demo/multi_tenant.demo.ts create mode 100644 modules/example-node/src/kms-hierarchical-keyring/caching_cmm.ts create mode 100644 modules/example-node/src/kms-hierarchical-keyring/disable_commitment.ts create mode 100644 modules/example-node/src/kms-hierarchical-keyring/multi_keyring.ts create mode 100644 modules/example-node/src/kms-hierarchical-keyring/multi_tenancy.ts create mode 100644 modules/example-node/src/kms-hierarchical-keyring/simple.ts create mode 100644 modules/example-node/src/kms-hierarchical-keyring/stream.ts create mode 100644 modules/kdf-ctr-mode-node/.eslintrc.js create mode 100644 modules/kdf-ctr-mode-node/CHANGELOG.md create mode 100644 modules/kdf-ctr-mode-node/LICENSE create mode 100644 modules/kdf-ctr-mode-node/NOTICE create mode 100644 modules/kdf-ctr-mode-node/README.md create mode 100644 modules/kdf-ctr-mode-node/package.json create mode 100644 modules/kdf-ctr-mode-node/src/index.ts create mode 100644 modules/kdf-ctr-mode-node/src/kdfctr.ts create mode 100644 modules/kdf-ctr-mode-node/test/kdfctr.test.ts create mode 100644 modules/kdf-ctr-mode-node/test/testvectors.ts create mode 100644 modules/kdf-ctr-mode-node/tsconfig.json create mode 100644 modules/kdf-ctr-mode-node/tsconfig.module.json create mode 100644 modules/kms-keyring-node/src/constants.ts create mode 100644 modules/kms-keyring-node/src/kms_hkeyring_node.ts create mode 100644 modules/kms-keyring-node/src/kms_hkeyring_node_helpers.ts create mode 100644 modules/kms-keyring-node/test/fixtures.ts create mode 100644 modules/kms-keyring-node/test/kms_hkeyring_node.constructor.test.ts create mode 100644 modules/kms-keyring-node/test/kms_hkeyring_node.edk-order.test.ts create mode 100644 modules/kms-keyring-node/test/kms_hkeyring_node.helpers.test.ts create mode 100644 modules/kms-keyring-node/test/kms_hkeyring_node.ondecrypt.test.ts create mode 100644 modules/kms-keyring-node/test/kms_hkeyring_node.onencrypt.test.ts create mode 100644 modules/kms-keyring-node/test/kms_hkeyring_node.test.ts create mode 100644 modules/kms-keyring/src/branch_key_id_supplier.ts create mode 100644 modules/kms-keyring/test/branch_key_id_supplier.test.ts create mode 100644 modules/serialize/src/uuidv4_factory.ts create mode 100644 modules/serialize/test/uuidv4_factory.test.ts diff --git a/.gitignore b/.gitignore index 8427abc2a..f7d5ab223 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,4 @@ package.json.decrypt # they track the package.json version /modules/kms-keyring-browser/src/version.ts /modules/kms-keyring-node/src/version.ts +/modules/branch-keystore-node/src/version.ts \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index 1daf98589..6d7f93847 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,4 @@ [submodule "aws-encryption-sdk-specification"] path = aws-encryption-sdk-specification url = https://github.com/awslabs/aws-encryption-sdk-specification.git + branch = master diff --git a/aws-encryption-sdk-specification b/aws-encryption-sdk-specification index c35fbd91b..bd9acf0c5 160000 --- a/aws-encryption-sdk-specification +++ b/aws-encryption-sdk-specification @@ -1 +1 @@ -Subproject commit c35fbd91b28303d69813119088c44b5006395eb4 +Subproject commit bd9acf0c59509a6e9809de35b7396450fa01d3a5 diff --git a/modules/branch-keystore-node/.eslintrc.js b/modules/branch-keystore-node/.eslintrc.js new file mode 100644 index 000000000..1c61b83da --- /dev/null +++ b/modules/branch-keystore-node/.eslintrc.js @@ -0,0 +1,12 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +module.exports = { + parserOptions: { + // There is an issue with @typescript-eslint/parser performance. + // It scales with the number of projects + // see https://github.com/typescript-eslint/typescript-eslint/issues/1192#issuecomment-596741806 + project: '../../tsconfig.lint.json', + tsconfigRootDir: __dirname, + } +} diff --git a/modules/branch-keystore-node/.gitignore b/modules/branch-keystore-node/.gitignore new file mode 100644 index 000000000..6498d2c9d --- /dev/null +++ b/modules/branch-keystore-node/.gitignore @@ -0,0 +1,3 @@ +/node_modules/ +/build/ +/.nyc_output \ No newline at end of file diff --git a/modules/branch-keystore-node/LICENSE b/modules/branch-keystore-node/LICENSE new file mode 100644 index 000000000..96ad5c3c1 --- /dev/null +++ b/modules/branch-keystore-node/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/modules/branch-keystore-node/NOTICE b/modules/branch-keystore-node/NOTICE new file mode 100644 index 000000000..88f7bea1e --- /dev/null +++ b/modules/branch-keystore-node/NOTICE @@ -0,0 +1,2 @@ +AWS Encryption SDK for Javascript +Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/modules/branch-keystore-node/README.md b/modules/branch-keystore-node/README.md new file mode 100644 index 000000000..507b91d55 --- /dev/null +++ b/modules/branch-keystore-node/README.md @@ -0,0 +1,31 @@ +# aws-encryption-sdk-javascript + +The AWS Encryption SDK for JavaScript is a client-side encryption library +designed to make it easy for everyone to encrypt +and decrypt data using industry standards and best practices. +It uses a data format compatible with the AWS Encryption SDKs in other languages. +For more information on the AWS Encryption SDKs in all languages, +see the [Developer Guide](https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/introduction.html). + +This package should only be used as part of the AWS Encryption SDK for Javascript. +For more information about the packages in this project +and how they can be used together, +see the [main node package readme](https://github.com/aws/aws-encryption-sdk-javascript/blob/master/modules/client-node/Readme.md) + +## Installing + +```sh +npm install @aws-crypto/branch-keystore-node +``` + +## Testing + +```sh +npm test +``` + +## License + +This SDK is distributed under the +[Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0), +see LICENSE.txt and NOTICE.txt for more information. diff --git a/modules/branch-keystore-node/package.json b/modules/branch-keystore-node/package.json new file mode 100644 index 000000000..23565b81a --- /dev/null +++ b/modules/branch-keystore-node/package.json @@ -0,0 +1,34 @@ +{ + "name": "@aws-crypto/branch-keystore-node", + "version": "4.0.0", + "scripts": { + "prepublishOnly": "npm run generate-version.ts; npm run build", + "generate-version.ts": "npx genversion --es6 src/version.ts", + "build": "tsc -b tsconfig.json && tsc -b tsconfig.module.json", + "lint": "run-s lint-*", + "lint-eslint": "eslint src/*.ts test/**/*.ts", + "lint-prettier": "prettier -c src/*.ts test/**/*.ts", + "mocha": "mocha --require ts-node/register test/**/*test.ts", + "test": "npm run lint && npm run coverage", + "coverage": "nyc -e .ts npm run mocha" + }, + "author": { + "name": "AWS Crypto Tools Team", + "email": "aws-crypto-tools-team@amazon.com", + "url": "https://github.com/aws/aws-encryption-sdk-javascript" + }, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/kms-keyring": "file:../kms-keyring", + "@aws-sdk/client-dynamodb": "^3.616.0", + "@aws-sdk/util-dynamodb": "^3.616.0", + "tslib": "^2.2.0" + }, + "sideEffects": false, + "main": "./build/main/src/index.js", + "module": "./build/module/src/index.js", + "types": "./build/main/src/index.d.ts", + "files": [ + "build/**/src/*" + ] +} diff --git a/modules/branch-keystore-node/src/branch_keystore.ts b/modules/branch-keystore-node/src/branch_keystore.ts new file mode 100644 index 000000000..0d901e9ae --- /dev/null +++ b/modules/branch-keystore-node/src/branch_keystore.ts @@ -0,0 +1,693 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { KmsConfig, KmsKeyConfig } from './kms_config' +import { KMSClient } from '@aws-sdk/client-kms' +import { DynamoDBClient } from '@aws-sdk/client-dynamodb' +import { + NodeBranchKeyMaterial, + immutableClass, + needs, + readOnlyProperty, +} from '@aws-crypto/material-management' +import { v4 } from 'uuid' +import { + constructBranchKeyMaterials, + decryptBranchKey, +} from './branch_keystore_helpers' +import { KMS_CLIENT_USER_AGENT, TABLE_FIELD } from './constants' + +import { + IBranchKeyStorage, + BranchKeyStoreNodeInput, + ActiveHierarchicalSymmetricVersion, + HierarchicalSymmetricVersion, +} from './types' +import { DynamoDBKeyStorage } from './dynamodb_key_storage' + +//= aws-encryption-sdk-specification/framework/branch-key-store.md#operations +//= type=implication +//# The Keystore MUST support the following operations: + +interface IBranchKeyStoreNode { + //= aws-encryption-sdk-specification/framework/branch-key-store.md#operations + //# - [GetActiveBranchKey](#getactivebranchkey) + getActiveBranchKey(branchKeyId: string): Promise + //= aws-encryption-sdk-specification/framework/branch-key-store.md#operations + //# - [GetBranchKeyVersion](#getbranchkeyversion) + getBranchKeyVersion( + branchKeyId: string, + branchKeyVersion: string + ): Promise + //= aws-encryption-sdk-specification/framework/branch-key-store.md#operations + //# - [GetKeyStoreInfo](#getkeystoreinfo) + getKeyStoreInfo(): KeyStoreInfoOutput +} + +//= aws-encryption-sdk-specification/framework/branch-key-store.md#getkeystoreinfo +//= type=implication +//# This MUST include: +//# - [keystore id](#keystore-id) +//# - [keystore name](#table-name) +//# - [logical Keystore name](#logical-keystore-name) +//# - [AWS KMS Grant Tokens](#aws-kms-grant-tokens) +//# - [AWS KMS Configuration](#aws-kms-configuration) + +export interface KeyStoreInfoOutput { + keystoreId: string + keystoreTableName: string + logicalKeyStoreName: string + grantTokens: string[] + kmsConfiguration: KmsConfig +} + +export class BranchKeyStoreNode implements IBranchKeyStoreNode { + public declare readonly logicalKeyStoreName: string + public declare readonly kmsConfiguration: Readonly + public declare readonly kmsClient: KMSClient + public declare readonly keyStoreId: string + public declare readonly grantTokens?: ReadonlyArray + public declare readonly storage: IBranchKeyStorage + + constructor({ + logicalKeyStoreName, + storage, + keyManagement, + kmsConfiguration, + keyStoreId, + }: BranchKeyStoreNodeInput) { + /* Precondition: Logical keystore name must be a string */ + needs( + typeof logicalKeyStoreName === 'string', + 'Logical keystore name must be a string' + ) + + needs(kmsConfiguration, 'AWS KMS Configuration required') + readOnlyProperty( + this, + 'kmsConfiguration', + new KmsKeyConfig(kmsConfiguration) + ) + + /* Precondition: KMS client must be a KMSClient */ + if (keyManagement?.kmsClient) { + needs( + keyManagement.kmsClient instanceof KMSClient, + 'KMS client must be a KMSClient' + ) + } + + if ( + 'getEncryptedActiveBranchKey' in storage && + 'getEncryptedBranchKeyVersion' in storage + ) { + // JS does structural typing. + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#initialization + //# If [Storage](#storage) is configured with [KeyStorage](#keystorage) + //# then this MUST be the configured [KeyStorage interface](./key-store/key-storage.md#interface). + this.storage + } else { + //= aws-encryption-sdk-specification/framework/branch-key-store.md#initialization + //# If [Storage](#storage) is not configured with [KeyStorage](#keystorage) + //# a [default key storage](./key-store/default-key-storage.md#initialization) MUST be created. + + needs( + !storage.ddbClient || + (storage.ddbClient as any) instanceof DynamoDBClient, + 'DDB client must be a DynamoDBClient' + ) + this.storage = new DynamoDBKeyStorage({ + //= aws-encryption-sdk-specification/framework/branch-key-store.md#initialization + //# This constructed [default key storage](./key-store/default-key-storage.md#initialization) + //# MUST be configured with either the [Table Name](#table-name) or the [DynamoDBTable](#dynamodbtable) table name + //# depending on which one is configured. + ddbTableName: storage.ddbTableName, + //= aws-encryption-sdk-specification/framework/branch-key-store.md#initialization + //# This constructed [default key storage](./key-store/default-key-storage.md#overview) + //# MUST be configured with the provided [logical keystore name](#logical-keystore-name). + logicalKeyStoreName, + ddbClient: + //= aws-encryption-sdk-specification/framework/branch-key-store.md#initialization + //# This constructed [default key storage](./key-store/default-key-storage.md#initialization) + //# MUST be configured with either the [DynamoDb Client](#dynamodb-client), the DDB client in the [DynamoDBTable](#dynamodbtable) + //# or a constructed DDB client depending on what is configured. + storage.ddbClient instanceof DynamoDBClient + ? storage.ddbClient + : new DynamoDBClient({ + //= aws-encryption-sdk-specification/framework/branch-key-store.md#initialization + //# If a DDB client needs to be constructed and the AWS KMS Configuration is KMS Key ARN or KMS MRKey ARN, + //# a new DynamoDb client MUST be created with the region of the supplied KMS ARN. + //# + //# If a DDB client needs to be constructed and the AWS KMS Configuration is Discovery, + //# a new DynamoDb client MUST be created with the default configuration. + //# + //# If a DDB client needs to be constructed and the AWS KMS Configuration is MRDiscovery, + //# a new DynamoDb client MUST be created with the region configured in the MRDiscovery. + region: this.kmsConfiguration.getRegion(), + }), + }) + } + readOnlyProperty(this, 'storage', this.storage) + + needs( + logicalKeyStoreName == this.storage.getKeyStorageInfo().logicalName, + 'Configured logicalKeyStoreName does not match configured storage interface.' + ) + + /* Precondition: Keystore id must be a string */ + if (keyStoreId) { + needs(typeof keyStoreId === 'string', 'Keystore id must be a string') + } else { + // ensure it's strictly undefined and not some other falsey value + keyStoreId = undefined + } + + /* Precondition: Grant tokens must be a string array */ + if (keyManagement?.grantTokens) { + needs( + Array.isArray(keyManagement.grantTokens) && + keyManagement.grantTokens.every( + (grantToken) => typeof grantToken === 'string' + ), + 'Grant tokens must be a string array' + ) + } + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#keystore-id + //# The Identifier for this KeyStore. + //# If one is not supplied, then a [version 4 UUID](https://www.ietf.org/rfc/rfc4122.txt) MUST be used. + readOnlyProperty(this, 'keyStoreId', keyStoreId ? keyStoreId : v4()) + /* Postcondition: If unprovided, the keystore id is a generated valid uuidv4 */ + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#aws-kms-grant-tokens + //# A list of AWS KMS [grant tokens](https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#grant_token). + readOnlyProperty( + this, + 'grantTokens', + keyManagement?.grantTokens || undefined + ) + /* Postcondition: If unprovided, the grant tokens are undefined */ + + // TODO: when other KMS configuration types/classes are supported for the keystore, + // verify the configuration object type to determine how we instantiate the + // KMS client. This will ensure safe type casting. + readOnlyProperty( + this, + 'kmsClient', + //= aws-encryption-sdk-specification/framework/branch-key-store.md#initialization + //# If no AWS KMS client is provided one MUST be constructed. + keyManagement?.kmsClient || + new KMSClient({ + //= aws-encryption-sdk-specification/framework/branch-key-store.md#initialization + //# If AWS KMS client needs to be constructed and the AWS KMS Configuration is KMS Key ARN or KMS MRKey ARN, + //# a new AWS KMS client MUST be created with the region of the supplied KMS ARN. + //# + //# If AWS KMS client needs to be constructed and the AWS KMS Configuration is Discovery, + //# a new AWS KMS client MUST be created with the default configuration. + //# + //# If AWS KMS client needs to be constructed and the AWS KMS Configuration is MRDiscovery, + //# a new AWS KMS client MUST be created with the region configured in the MRDiscovery. + region: this.kmsConfiguration.getRegion(), + //= aws-encryption-sdk-specification/framework/branch-key-store.md#initialization + //# On initialization the KeyStore SHOULD + //# append a user agent string to the AWS KMS SDK Client with + //# the value `aws-kms-hierarchy`. + customUserAgent: KMS_CLIENT_USER_AGENT, + }) + ) + /* Postcondition: If unprovided, the KMS client is configured */ + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#logical-keystore-name + //# This name is cryptographically bound to all data stored in this table, + //# and logically separates data between different tables. + //# + //# The logical keystore name MUST be bound to every created key. + //# + //# There needs to be a one to one mapping between DynamoDB Table Names and the Logical KeyStore Name. + //# This value can be set to the DynamoDB table name itself, but does not need to. + //# + //# Controlling this value independently enables restoring from DDB table backups + //# even when the table name after restoration is not exactly the same. + needs(logicalKeyStoreName, 'Logical Keystore name required') + readOnlyProperty(this, 'logicalKeyStoreName', logicalKeyStoreName) + + // make this instance immutable + Object.freeze(this) + } + + async getActiveBranchKey( + branchKeyId: string + ): Promise { + //= aws-encryption-sdk-specification/framework/branch-key-store.md#getactivebranchkey + //# On invocation, the caller: + //# + //# - MUST supply a `branch-key-id` + needs( + branchKeyId && typeof branchKeyId === 'string', + 'MUST supply a string branch key id' + ) + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#getactivebranchkey + //# GetActiveBranchKey MUST get the active version for the branch key id from the keystore + //# by calling the configured [KeyStorage interface's](./key-store/key-storage.md#interface) + //# [GetEncryptedActiveBranchKey](./key-store/key-storage.md#getencryptedactivebranchkey) + //# using the supplied `branch-key-id`. + const activeEncryptedBranchKey = + await this.storage.getEncryptedActiveBranchKey(branchKeyId) + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#getactivebranchkey + //# Because the storage interface can be a custom implementation the key store needs to verify correctness. + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#getactivebranchkey + //# GetActiveBranchKey MUST verify that the returned EncryptedHierarchicalKey MUST have the requested `branch-key-id`. + needs( + activeEncryptedBranchKey.branchKeyId == branchKeyId, + 'Unexpected branch key id.' + ) + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#getactivebranchkey + //# GetActiveBranchKey MUST verify that the returned EncryptedHierarchicalKey is an ActiveHierarchicalSymmetricVersion. + needs( + activeEncryptedBranchKey.type instanceof + ActiveHierarchicalSymmetricVersion, + 'Unexpected type. Not an version record.' + ) + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#getactivebranchkey + //# GetActiveBranchKey MUST verify that the returned EncryptedHierarchicalKey MUST have a logical table name equal to the configured logical table name. + needs( + activeEncryptedBranchKey.encryptionContext[TABLE_FIELD] == + this.logicalKeyStoreName, + 'Unexpected logical table name.' + ) + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#getactivebranchkey + //# If the branch key fails to decrypt, GetActiveBranchKey MUST fail. + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#getactivebranchkey + //# The operation MUST decrypt the EncryptedHierarchicalKey according to the [AWS KMS Branch Key Decryption](#aws-kms-branch-key-decryption) section. + const branchKey = await decryptBranchKey(this, activeEncryptedBranchKey) + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#getactivebranchkey + //# This GetActiveBranchKey MUST construct [branch key materials](./structures.md#branch-key-materials) + //# according to [Branch Key Materials From Authenticated Encryption Context](#branch-key-materials-from-authenticated-encryption-context). + const branchKeyMaterials = constructBranchKeyMaterials( + branchKey, + activeEncryptedBranchKey + ) + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#getactivebranchkey + //# This operation MUST return the constructed [branch key materials](./structures.md#branch-key-materials). + return branchKeyMaterials + } + + async getBranchKeyVersion( + branchKeyId: string, + branchKeyVersion: string + ): Promise { + //= aws-encryption-sdk-specification/framework/branch-key-store.md#getbranchkeyversion + //# On invocation, the caller: + //# + //# - MUST supply a `branch-key-id` + //# - MUST supply a `branchKeyVersion` + needs( + branchKeyId && typeof branchKeyId === 'string', + 'MUST supply a string branch key id' + ) + needs( + branchKeyId && branchKeyVersion, + 'MUST supply a string branch key version' + ) + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#getbranchkeyversion + //= type=implication + //# GetBranchKeyVersion MUST get the requested version for the branch key id from the keystore + //# by calling the configured [KeyStorage interface's](./key-store/key-storage.md#interface) + //# [GetEncryptedActiveBranchKey](./key-store/key-storage.md#getencryptedbranchkeyversion) + //# using the supplied `branch-key-id`. + const encryptedBranchKey = await this.storage.getEncryptedBranchKeyVersion( + branchKeyId, + branchKeyVersion + ) + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#getbranchkeyversion + //# GetBranchKeyVersion MUST verify that the returned EncryptedHierarchicalKey MUST have the requested `branch-key-id`. + needs( + encryptedBranchKey.branchKeyId == branchKeyId, + 'Unexpected branch key id.' + ) + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#getbranchkeyversion + //# GetBranchKeyVersion MUST verify that the returned EncryptedHierarchicalKey MUST have the requested `branchKeyVersion`. + needs( + encryptedBranchKey.type.version == branchKeyVersion, + 'Unexpected branch key id.' + ) + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#getbranchkeyversion + //# GetActiveBranchKey MUST verify that the returned EncryptedHierarchicalKey is an HierarchicalSymmetricVersion. + needs( + encryptedBranchKey.type instanceof HierarchicalSymmetricVersion, + 'Unexpected type. Not an version record.' + ) + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#getbranchkeyversion + //# GetBranchKeyVersion MUST verify that the returned EncryptedHierarchicalKey MUST have a logical table name equal to the configured logical table name. + needs( + encryptedBranchKey.encryptionContext[TABLE_FIELD] == + this.logicalKeyStoreName, + 'Unexpected logical table name.' + ) + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#getbranchkeyversion + //# If the branch key fails to decrypt, this operation MUST fail. + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#getbranchkeyversion + //# The operation MUST decrypt the branch key according to the [AWS KMS Branch Key Decryption](#aws-kms-branch-key-decryption) section. + const branchKey = await decryptBranchKey(this, encryptedBranchKey) + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#getbranchkeyversion + //# This GetBranchKeyVersion MUST construct [branch key materials](./structures.md#branch-key-materials) + //# according to [Branch Key Materials From Authenticated Encryption Context](#branch-key-materials-from-authenticated-encryption-context). + const branchKeyMaterials = constructBranchKeyMaterials( + branchKey, + encryptedBranchKey + ) + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#getbranchkeyversion + //# This operation MUST return the constructed [branch key materials](./structures.md#branch-key-materials). + return branchKeyMaterials + } + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#getkeystoreinfo + //= type=implication + //# This operation MUST return the keystore information in this keystore configuration. + getKeyStoreInfo(): KeyStoreInfoOutput { + return { + keystoreId: this.keyStoreId, + //= aws-encryption-sdk-specification/framework/branch-key-store.md#getkeystoreinfo + //= type=implication + //# The [keystore name](#table-name) MUST be obtained + //# from the configured [KeyStorage](./key-store/key-storage.md#interface) + //# by calling [GetKeyStorageInfo](./key-store/key-storage.md#getkeystorageinfo). + keystoreTableName: this.storage.getKeyStorageInfo().name, + logicalKeyStoreName: this.logicalKeyStoreName, + grantTokens: !!this.grantTokens ? this.grantTokens.slice() : [], + kmsConfiguration: this.kmsConfiguration._config, + } + } +} + +immutableClass(BranchKeyStoreNode) + +// type guard +export function isIBranchKeyStoreNode( + keyStore: any +): keyStore is BranchKeyStoreNode { + return keyStore instanceof BranchKeyStoreNode +} + +// The JS implementation is not encumbered with the legacy construction +// by passing DDB clients et al. +// So it can be simplified. + +//= aws-encryption-sdk-specification/framework/branch-key-store.md#initialization +//= type=exception +//# - [AWS KMS Grant Tokens](#aws-kms-grant-tokens) + +//= aws-encryption-sdk-specification/framework/branch-key-store.md#initialization +//= type=exception +//# - [DynamoDb Client](#dynamodb-client) + +//= aws-encryption-sdk-specification/framework/branch-key-store.md#initialization +//= type=exception +//# - [Table Name](#table-name) +//# - [KMS Client](#kms-client) + +//= aws-encryption-sdk-specification/framework/branch-key-store.md#operations +//= type=exception +//# - [CreateKeyStore](#createkeystore) +//# - [CreateKey](#createkey) +//# - [VersionKey](#versionkey) + +//= aws-encryption-sdk-specification/framework/branch-key-store.md#operations +//= type=exception +//# - [GetBeaconKey](#getbeaconkey) + +// Only `Storage` is defined as as input +// because JS was only released with this option. + +//= aws-encryption-sdk-specification/framework/branch-key-store.md#initialization +//= type=exception +//# If neither [Storage](#storage) nor [Table Name](#table-name) is configured initialization MUST fail. +//# If both [Storage](#storage) and [Table Name](#table-name) are configured initialization MUST fail. +//# If both [Storage](#storage) and [DynamoDb Client](#dynamodb-client) are configured initialization MUST fail. + +// Only `KeyManagement` is defined as as input +// because JS was only released with this option. + +//= aws-encryption-sdk-specification/framework/branch-key-store.md#initialization +//= type=exception +//# If both [KeyManagement](#keymanagement) and [KMS Client](#kms-client) are configured initialization MUST fail. +//# If both [KeyManagement](#keymanagement) and [Grant Tokens](#aws-kms-grant-tokens) are configured initialization MUST fail. + +//= aws-encryption-sdk-specification/framework/branch-key-store.md#createkeystore +//= type=exception +//# If a [table Name](#table-name) was not configured then CreateKeyStore MUST fail. +//# +//# This operation MUST first calls the DDB::DescribeTable API with the configured `tableName`. +//# +//# If the response is successful, this operation validates that the table has the expected +//# [KeySchema](#keyschema) as defined below. +//# If the [KeySchema](#keyschema) does not match +//# this operation MUST yield an error. +//# The table MAY have additional information, +//# like GlobalSecondaryIndex defined. +//# +//# If the client responds with a `ResourceNotFoundException`, +//# then this operation MUST continue and +//# MUST call [AWS DDB CreateTable](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_CreateTable.html) +//# with the following specifics: +//# +//# - TableName is the configured tableName. +//# - [KeySchema](#keyschema) as defined below. +//# +//# If the operation fails to create table, the operation MUST fail. +//# +//# If the operation successfully creates a table, the operation MUST return the AWS DDB Table Arn +//# back to the caller. + +//= aws-encryption-sdk-specification/framework/branch-key-store.md#createkey +//= type=exception +//# The CreateKey caller MUST provide: +//# +//# - An optional branch key id +//# - An optional encryption context +//# +//# If an optional branch key id is provided +//# and no encryption context is provided this operation MUST fail. +//# +//# If the Keystore's KMS Configuration is `Discovery` or `MRDiscovery`, +//# this operation MUST fail. +//# +//# If no branch key id is provided, +//# then this operation MUST create a [version 4 UUID](https://www.ietf.org/rfc/rfc4122.txt) +//# to be used as the branch key id. +//# +//# This operation MUST create a [branch key](structures.md#branch-key) and a [beacon key](structures.md#beacon-key) according to +//# the [Branch Key and Beacon Key Creation](#branch-key-and-beacon-key-creation) section. +//# +//# If creation of the keys are successful, +//# then the key store MUST call the configured [KeyStorage interface's](./key-store/key-storage.md#interface) +//# [WriteNewEncryptedBranchKey](./key-store/key-storage.md#writenewencryptedbranchkey) with these 3 [EncryptedHierarchicalKeys](./key-store/key-storage.md#encryptedhierarchicalkey). +//# +//# If writing to the keystore succeeds, +//# the operation MUST return the branch-key-id that maps to both +//# the branch key and the beacon key. +//# +//# Otherwise, this operation MUST yield an error. + +//= aws-encryption-sdk-specification/framework/branch-key-store.md#branch-key-and-beacon-key-creation +//= type=exception +//# To create a branch key, this operation MUST take the following: +//# +//# - `branchKeyId`: The identifier +//# - `encryptionContext`: Additional encryption context to bind to the created keys +//# +//# This operation needs to generate the following: +//# +//# - `version`: a new guid. This guid MUST be [version 4 UUID](https://www.ietf.org/rfc/rfc4122.txt) +//# - `timestamp`: a timestamp for the current time. +//# This timestamp MUST be in ISO 8601 format in UTC, to microsecond precision (e.g. “YYYY-MM-DDTHH:mm:ss.ssssssZ“) +//# +//# The wrapped Branch Keys, DECRYPT_ONLY and ACTIVE, MUST be created according to [Wrapped Branch Key Creation](#wrapped-branch-key-creation). +//# +//# To create a beacon key, this operation will continue to use the `branchKeyId` and `timestamp` as the [Branch Key](structures.md#branch-key). +//# +//# The operation MUST call [AWS KMS API GenerateDataKeyWithoutPlaintext](https://docs.aws.amazon.com/kms/latest/APIReference/API_GenerateDataKeyWithoutPlaintext.html). +//# The call to AWS KMS GenerateDataKeyWithoutPlaintext MUST use the configured AWS KMS client to make the call. +//# The operation MUST call AWS KMS GenerateDataKeyWithoutPlaintext with a request constructed as follows: +//# +//# - `KeyId` MUST be [compatible with](#aws-key-arn-compatibility) the configured KMS Key in the [AWS KMS Configuration](#aws-kms-configuration) for this keystore. +//# - `NumberOfBytes` MUST be 32. +//# - `EncryptionContext` MUST be the [encryption context for beacon keys](#beacon-key-encryption-context). +//# - `GrantTokens` MUST be this keystore's [grant tokens](https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#grant_token). +//# +//# If the call to AWS KMS GenerateDataKeyWithoutPlaintext succeeds, +//# the operation MUST use the `CiphertextBlob` as the wrapped Beacon Key. + +//= aws-encryption-sdk-specification/framework/branch-key-store.md#wrapped-branch-key-creation +//= type=exception +//# The operation MUST call [AWS KMS API GenerateDataKeyWithoutPlaintext](https://docs.aws.amazon.com/kms/latest/APIReference/API_GenerateDataKeyWithoutPlaintext.html). +//# The call to AWS KMS GenerateDataKeyWithoutPlaintext MUST use the configured AWS KMS client to make the call. +//# The operation MUST call AWS KMS GenerateDataKeyWithoutPlaintext with a request constructed as follows: +//# +//# - `KeyId` MUST be [compatible with](#aws-key-arn-compatibility) the configured KMS Key in the [AWS KMS Configuration](#aws-kms-configuration) for this keystore. +//# - `NumberOfBytes` MUST be 32. +//# - `EncryptionContext` MUST be the [DECRYPT_ONLY encryption context for branch keys](#decrypt_only-encryption-context). +//# - GenerateDataKeyWithoutPlaintext `GrantTokens` MUST be this keystore's [grant tokens](https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#grant_token). +//# +//# If the call to AWS KMS GenerateDataKeyWithoutPlaintext succeeds, +//# the operation MUST use the GenerateDataKeyWithoutPlaintext result `CiphertextBlob` +//# as the wrapped DECRYPT_ONLY Branch Key. +//# +//# The operation MUST call [AWS KMS API ReEncrypt](https://docs.aws.amazon.com/kms/latest/APIReference/API_ReEncrypt.html) +//# with a request constructed as follows: +//# +//# - `SourceEncryptionContext` MUST be the [DECRYPT_ONLY encryption context for branch keys](#decrypt_only-encryption-context). +//# - `SourceKeyId` MUST be [compatible with](#aws-key-arn-compatibility) the configured KMS Key in the [AWS KMS Configuration](#aws-kms-configuration) for this keystore. +//# - `CiphertextBlob` MUST be the wrapped DECRYPT_ONLY Branch Key. +//# - ReEncrypt `GrantTokens` MUST be this keystore's [grant tokens](https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#grant_token). +//# - `DestinationKeyId` MUST be [compatible with](#aws-key-arn-compatibility) the configured KMS Key in the [AWS KMS Configuration](#aws-kms-configuration) for this keystore. +//# - `DestinationEncryptionContext` MUST be the [ACTIVE encryption context for branch keys](#active-encryption-context). +//# +//# If the call to AWS KMS ReEncrypt succeeds, +//# the operation MUST use the ReEncrypt result `CiphertextBlob` +//# as the wrapped ACTIVE Branch Key. + +//= aws-encryption-sdk-specification/framework/branch-key-store.md#active-encryption-context +//= type=exception +//# The ACTIVE branch key is a copy of the DECRYPT_ONLY with the same `version`. +//# It is structured slightly differently so that the active version can be accessed quickly. +//# +//# In addition to the [encryption context](#encryption-context): +//# +//# The ACTIVE encryption context value of the `type` attribute MUST equal to `"branch:ACTIVE"`. +//# The ACTIVE encryption context MUST have a `version` attribute. +//# The `version` attribute MUST store the branch key version formatted like `"branch:version:"` + `version`. + +//= aws-encryption-sdk-specification/framework/branch-key-store.md#decrypt-only-encryption-context +//= type=exception +//# In addition to the [encryption context](#encryption-context): +//# +//# The DECRYPT_ONLY encryption context MUST NOT have a `version` attribute. +//# The `type` attribute MUST stores the branch key version formatted like `"branch:version:"` + `version`. + +//= aws-encryption-sdk-specification/framework/branch-key-store.md#beacon-key-encryption-context +//= type=exception +//# In addition to the [encryption context](#encryption-context): +//# +//# The Beacon key encryption context value of the `type` attribute MUST equal to `"beacon:ACTIVE"`. +//# The Beacon key encryption context MUST NOT have a `version` attribute. + +//= aws-encryption-sdk-specification/framework/branch-key-store.md#versionkey +//= type=exception +//# - MUST supply a `branch-key-id` +//# +//# If the Keystore's KMS Configuration is `Discovery` or `MRDiscovery`, +//# this operation MUST immediately fail. +//# +//# VersionKey MUST first get the active version for the branch key from the keystore +//# by calling the configured [KeyStorage interface's](./key-store/key-storage.md#interface) +//# [GetEncryptedActiveBranchKey](./key-store/key-storage.md##getencryptedactivebranchkey) +//# using the `branch-key-id`. +//# +//# The `KmsArn` of the [EncryptedHierarchicalKey](./key-store/key-storage.md##encryptedhierarchicalkey) +//# MUST be [compatible with](#aws-key-arn-compatibility) +//# the configured `KMS ARN` in the [AWS KMS Configuration](#aws-kms-configuration) for this keystore. +//# +//# Because the storage interface can be a custom implementation the key store needs to verify correctness. +//# +//# VersionKey MUST verify that the returned EncryptedHierarchicalKey MUST have the requested `branch-key-id`. +//# VersionKey MUST verify that the returned EncryptedHierarchicalKey is an ActiveHierarchicalSymmetricVersion. +//# VersionKey MUST verify that the returned EncryptedHierarchicalKey MUST have a logical table name equal to the configured logical table name. +//# +//# The `kms-arn` stored in the table MUST NOT change as a result of this operation, +//# even if the KeyStore is configured with a `KMS MRKey ARN` that does not exactly match the stored ARN. +//# If such were allowed, clients using non-MRK KeyStores might suddenly stop working. +//# +//# The [EncryptedHierarchicalKey](./key-store/key-storage.md##encryptedhierarchicalkey) +//# MUST be authenticated according to [authenticating a keystore item](#authenticating-an-encryptedhierarchicalkey). +//# If the item fails to authenticate this operation MUST fail. +//# +//# The wrapped Branch Keys, DECRYPT_ONLY and ACTIVE, MUST be created according to [Wrapped Branch Key Creation](#wrapped-branch-key-creation). +//# +//# If creation of the keys are successful, +//# then the key store MUST call the configured [KeyStorage interface's](./key-store/key-storage.md#interface) +//# [WriteNewEncryptedBranchKeyVersion](./key-store/key-storage.md##writenewencryptedbranchkeyversion) +//# with these 2 [EncryptedHierarchicalKeys](./key-store/key-storage.md##encryptedhierarchicalkey). +//# +//# If the [WriteNewEncryptedBranchKeyVersion](./key-store/key-storage.md##writenewencryptedbranchkeyversion) is successful, +//# this operation MUST return a successful response containing no additional data. +//# Otherwise, this operation MUST yield an error. + +//= aws-encryption-sdk-specification/framework/branch-key-store.md#authenticating-an-encryptedhierarchicalkey +//= type=exception +//# The operation MUST use the configured `KMS SDK Client` to authenticate the value of the keystore item. +//# +//# The operation MUST call [AWS KMS API ReEncrypt](https://docs.aws.amazon.com/kms/latest/APIReference/API_ReEncrypt.html) +//# with a request constructed as follows: +//# +//# - `SourceEncryptionContext` MUST be the [encryption context](#encryption-context) of the EncryptedHierarchicalKey to be authenticated +//# - `SourceKeyId` MUST be [compatible with](#aws-key-arn-compatibility) the configured KMS Key in the [AWS KMS Configuration](#aws-kms-configuration) for this keystore. +//# - `CiphertextBlob` MUST be the `CiphertextBlob` attribute value on the EncryptedHierarchicalKey to be authenticated +//# - `GrantTokens` MUST be the configured [grant tokens](https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#grant_token). +//# - `DestinationKeyId` MUST be [compatible with](#aws-key-arn-compatibility) the configured KMS Key in the [AWS KMS Configuration](#aws-kms-configuration) for this keystore. +//# - `DestinationEncryptionContext` MUST be the [encryption context](#encryption-context) of the EncryptedHierarchicalKey to be authenticated + +// Custom EC is only _added_ during construction. +// in all other cases, the EC will be associated with the existing records. + +//= aws-encryption-sdk-specification/framework/branch-key-store.md#custom-encryption-context +//= type=exception +//# If custom [encryption context](./structures.md#encryption-context-3) +//# is associated with the branch key these values MUST be added to the AWS KMS encryption context. +//# To avoid name collisions each added attribute from the custom [encryption context](./structures.md#encryption-context-3) +//# MUST be prefixed with `aws-crypto-ec:`. +//# Across all versions of a Branch Key, the custom encryption context MUST be equal. + +//= aws-encryption-sdk-specification/framework/branch-key-store.md#keyschema +//= type=exception +//# The following KeySchema MUST be configured on the table: +//# +//# | AttributeName | KeyType | Type | +//# | ------------- | --------- | ---- | +//# | branch-key-id | Partition | S | +//# | type | Sort | S | + +//= aws-encryption-sdk-specification/framework/branch-key-store.md#getbeaconkey +//= type=exception +//# On invocation, the caller: +//# +//# - MUST supply a `branch-key-id` +//# +//# GetBeaconKey MUST get the requested beacon key from the keystore +//# by calling the configured [KeyStorage interface's](./key-store/key-storage.md#interface) +//# [GetEncryptedBeaconKey](./key-store/key-storage.md#getencryptedbeaconkey) +//# using the supplied `branch-key-id`. +//# +//# Because the storage interface can be a custom implementation the key store needs to verify correctness. +//# +//# GetBeaconKey MUST verify that the returned EncryptedHierarchicalKey MUST have the requested `branch-key-id`. +//# GetBeaconKey MUST verify that the returned EncryptedHierarchicalKey is an ActiveHierarchicalSymmetricBeacon. +//# GetBeaconKey MUST verify that the returned EncryptedHierarchicalKey MUST have a logical table name equal to the configured logical table name. +//# +//# The operation MUST decrypt the beacon key according to the [AWS KMS Branch Key Decryption](#aws-kms-branch-key-decryption) section. +//# +//# If the beacon key fails to decrypt, this operation MUST fail. +//# +//# This GetBeaconKey MUST construct [beacon key materials](./structures.md#beacon-key-materials) from the decrypted branch key material +//# and the `branchKeyId` from the returned `branch-key-id` field. +//# +//# This operation MUST return the constructed [beacon key materials](./structures.md#beacon-key-materials). diff --git a/modules/branch-keystore-node/src/branch_keystore_helpers.ts b/modules/branch-keystore-node/src/branch_keystore_helpers.ts new file mode 100644 index 000000000..25d49c097 --- /dev/null +++ b/modules/branch-keystore-node/src/branch_keystore_helpers.ts @@ -0,0 +1,361 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { GetItemCommand, DynamoDBClient } from '@aws-sdk/client-dynamodb' +import { KMSClient } from '@aws-sdk/client-kms' +import { + needs, + NodeBranchKeyMaterial, + EncryptionContext, +} from '@aws-crypto/material-management' +import { unmarshall } from '@aws-sdk/util-dynamodb' +import { BranchKeyItem, BranchKeyRecord } from './branch_keystore_structures' +import { EncryptedHierarchicalKey, BranchKeyEncryptionContext } from './types' +// import { IBranchKeyStoreNode } from './branch_keystore' +import { DecryptCommand } from '@aws-sdk/client-kms' +import { KmsKeyConfig } from './kms_config' +import { + PARTITION_KEY, + SORT_KEY, + TABLE_FIELD, + CUSTOM_ENCRYPTION_CONTEXT_FIELD_PREFIX, + BRANCH_KEY_IDENTIFIER_FIELD, + TYPE_FIELD, + KEY_CREATE_TIME_FIELD, + HIERARCHY_VERSION_FIELD, + KMS_FIELD, + BRANCH_KEY_FIELD, + BRANCH_KEY_ACTIVE_VERSION_FIELD, + BRANCH_KEY_TYPE_PREFIX, + BRANCH_KEY_ACTIVE_TYPE, + BEACON_KEY_TYPE_VALUE, + POTENTIAL_BRANCH_KEY_RECORD_FIELDS, +} from './constants' + +/** + * This utility function uses a partition and sort key to query for a single branch + * keystore record + * @param ddbClient + * @param ddbTableName + * @param partitionValue + * @param sortValue + * @returns A DDB response item representing the branch keystore record + * @throws 'Record not found in DynamoDB' if the query yields no hits for a + * branch key record + */ +export async function getBranchKeyItem( + { + ddbClient, + ddbTableName, + }: { + ddbClient: DynamoDBClient + ddbTableName: string + }, + partitionValue: string, + sortValue: string +): Promise { + // create a getItem command with the querying partition and sort keys + // send the query for DDB to run + // get the response + const response = await ddbClient.send( + new GetItemCommand({ + TableName: ddbTableName, + Key: { + [PARTITION_KEY]: { S: partitionValue }, + [SORT_KEY]: { S: sortValue }, + }, + }) + ) + // the response has an Item field if the branch keystore record was found + const responseItem = response.Item + // error out if there is not Item field (record not found) + needs( + responseItem, + `A branch key record with ${PARTITION_KEY}=${partitionValue} and ${SORT_KEY}=${sortValue} was not found in DynamoDB` + ) + // at this point, we got back a record so convert the DDB response item into + // a more JS-friendly object + return unmarshall(responseItem) +} + +/** + * This utility function validates the DDB response item against the required + * record fromat and transforms the item into a branch key record + * @param item is the DDB response item representing a branch keystore record + * @returns a validated branch key record abiding by the proper record format + * @throws `Branch keystore record does not contain a ${BRANCH_KEY_IDENTIFIER_FIELD} field of type string` + * @throws `Branch keystore record does not contain a valid ${TYPE_FIELD} field of type string` + * @throws `Branch keystore record does not contain a ${BRANCH_KEY_ACTIVE_VERSION_FIELD} field of type string` + * if the type field is "branch:ACTIVE" but there is no version field in the DDB + * response item + * @throws `Branch keystore record does not contain ${BRANCH_KEY_FIELD} field of type Uint8Array` + * @throws `Branch keystore record does not contain ${KMS_FIELD} field of type string` + * @throws `Branch keystore record does not contain ${KEY_CREATE_TIME_FIELD} field of type string` + * @throws `Branch keystore record does not contain ${HIERARCHY_VERSION_FIELD} field of type number` + * @throws `Custom encryption context key ${field} should be prefixed with ${CUSTOM_ENCRYPTION_CONTEXT_FIELD_PREFIX}` + * if there are additional fields within the response item that + * don't follow the proper custom encryption context key naming convention + */ +//= aws-encryption-sdk-specification/framework/key-store/dynamodb-key-storage.md#record-format +//# A branch key record MUST include the following key-value pairs: +export function validateBranchKeyRecord(item: BranchKeyItem): BranchKeyRecord { + //= aws-encryption-sdk-specification/framework/key-store/dynamodb-key-storage.md#record-format + //# 1. `branch-key-id` : Unique identifier for a branch key; represented as [AWS DDB String](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes) + needs( + BRANCH_KEY_IDENTIFIER_FIELD in item && + typeof item[BRANCH_KEY_IDENTIFIER_FIELD] === 'string', + `Branch keystore record does not contain a ${BRANCH_KEY_IDENTIFIER_FIELD} field of type string` + ) + + //= aws-encryption-sdk-specification/framework/key-store/dynamodb-key-storage.md#record-format + //# 1. `type` : One of the following; represented as [AWS DDB String](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes) + //# - The string literal `"beacon:ACTIVE"`. Then `enc` is the wrapped beacon key. + //# - The string `"branch:version:"` + `version`, where `version` is the Branch Key Version. Then `enc` is the wrapped branch key. + //# - The string literal `"branch:ACTIVE"`. Then `enc` is the wrapped beacon key of the active version. Then + needs( + TYPE_FIELD in item && + typeof item[TYPE_FIELD] === 'string' && + (item[TYPE_FIELD] === BRANCH_KEY_ACTIVE_TYPE || + item[TYPE_FIELD].startsWith(BRANCH_KEY_TYPE_PREFIX) || + item[TYPE_FIELD] === BEACON_KEY_TYPE_VALUE), + `Branch keystore record does not contain a valid ${TYPE_FIELD} field of type string` + ) + + //= aws-encryption-sdk-specification/framework/key-store/dynamodb-key-storage.md#record-format + //# 1. `version` : Only exists if `type` is the string literal `"branch:ACTIVE"`. + //# Then it is the Branch Key Version. represented as [AWS DDB String](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes) + if (item[TYPE_FIELD] === BRANCH_KEY_ACTIVE_TYPE) { + needs( + BRANCH_KEY_ACTIVE_VERSION_FIELD in item && + typeof item[BRANCH_KEY_ACTIVE_VERSION_FIELD] === 'string', + `Branch keystore record does not contain a ${BRANCH_KEY_ACTIVE_VERSION_FIELD} field of type string` + ) + } + + //= aws-encryption-sdk-specification/framework/key-store/dynamodb-key-storage.md#record-format + //# 1. `enc` : Encrypted version of the key; + //# represented as [AWS DDB Binary](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes) + needs( + BRANCH_KEY_FIELD in item && item[BRANCH_KEY_FIELD] instanceof Uint8Array, + `Branch keystore record does not contain ${BRANCH_KEY_FIELD} field of type Uint8Array` + ) + + //= aws-encryption-sdk-specification/framework/key-store/dynamodb-key-storage.md#record-format + //# 1. `kms-arn`: The AWS KMS Key ARN used to generate the `enc` value. + //# represented as [AWS DDB String](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes) + needs( + KMS_FIELD in item && typeof item[KMS_FIELD] === 'string', + `Branch keystore record does not contain ${KMS_FIELD} field of type string` + ) + + //= aws-encryption-sdk-specification/framework/key-store/dynamodb-key-storage.md#record-format + //# 1. `create-time`: Timestamp in ISO 8601 format in UTC, to microsecond precision. + //# Represented as [AWS DDB String](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes) + needs( + KEY_CREATE_TIME_FIELD in item && + typeof item[KEY_CREATE_TIME_FIELD] === 'string', + `Branch keystore record does not contain ${KEY_CREATE_TIME_FIELD} field of type string` + ) + + //= aws-encryption-sdk-specification/framework/key-store/dynamodb-key-storage.md#record-format + //# 1. `hierarchy-version`: Version of the hierarchical keyring; + //# represented as [AWS DDB Number](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes) + needs( + HIERARCHY_VERSION_FIELD in item && + typeof item[HIERARCHY_VERSION_FIELD] === 'number', + `Branch keystore record does not contain ${HIERARCHY_VERSION_FIELD} field of type number` + ) + + //= aws-encryption-sdk-specification/framework/key-store/dynamodb-key-storage.md#record-format + //# A branch key record MAY include [custom encryption context](../branch-key-store.md#custom-encryption-context) key-value pairs. + //# These attributes should be prefixed with `aws-crypto-ec:` the same way they are for [AWS KMS encryption context](../branch-key-store.md#encryption-context). + for (const field in item) { + if (!POTENTIAL_BRANCH_KEY_RECORD_FIELDS.includes(field)) { + needs( + field.startsWith(CUSTOM_ENCRYPTION_CONTEXT_FIELD_PREFIX), + `Custom encryption context key ${field} should be prefixed with ${CUSTOM_ENCRYPTION_CONTEXT_FIELD_PREFIX}` + ) + } + } + + // serialize the DDB response item as a more well-defined and validated branch + // key record object + return Object.assign({}, item) as BranchKeyRecord +} + +/** + * This utility function builds an authenticated encryption context from the DDB + * response item + * @param logicalKeyStoreName + * @param branchKeyRecord + * @returns authenticated encryption context + */ +export function constructAuthenticatedEncryptionContext( + //= aws-encryption-sdk-specification/framework/key-store/dynamodb-key-storage.md#logical-keystore-name + //# It is not stored on the items in the so it MUST be added + //# to items retrieved from the table. + { logicalKeyStoreName }: { logicalKeyStoreName: string }, + branchKeyRecord: BranchKeyRecord +): BranchKeyEncryptionContext { + //= aws-encryption-sdk-specification/framework/branch-key-store.md#encryption-context + //# This section describes how the AWS KMS encryption context is built + //# from an [encrypted hierarchical key](./key-store/key-storage.md#encryptedhierarchicalkey). + //# + //# The following encryption context keys are shared: + //# + //# - MUST have a `branch-key-id` attribute + //# - The `branch-key-id` field MUST not be an empty string + //# - MUST have a `type` attribute + //# - The `type` field MUST not be an empty string + //# - MUST have a `create-time` attribute + //# - MUST have a `tablename` attribute to store the logicalKeyStoreName + //# - MUST have a `kms-arn` attribute + //# - MUST have a `hierarchy-version` + //# - MUST NOT have a `enc` attribute + //# + //# Any additionally attributes in the EncryptionContext + //# of the [encrypted hierarchical key](./key-store/key-storage.md#encryptedhierarchicalkey) + //# MUST be added to the encryption context. + //# + + // the encryption context is a string to string map, so serialize the branch + // key record to this form + // filter out the enc field + // add in the tablename key-value pair + const encryptionContext: BranchKeyEncryptionContext = { + ...Object.fromEntries( + Object.entries(branchKeyRecord) + .map(([key, value]) => [key, value.toString()]) + .filter(([key]) => key !== BRANCH_KEY_FIELD) + ), + [TABLE_FIELD]: logicalKeyStoreName, + } + return encryptionContext +} + +/** + * This utility function decrypts a branch key via KMS + * @param kmsConfiguration + * @param grantTokens + * @param kmsClient + * @param branchKeyRecord + * @param authenticatedEncryptionContext + * @returns the unencrypted branch key + * @throws 'KMS ARN from DDB response item MUST be compatible with the configured KMS Key in the AWS KMS Configuration for this keystore' + * @throws 'KMS branch key decryption failed' if the KMS response does not + * contain a plaintext field representing the plaintext branch data key + */ +export async function decryptBranchKey( + { + kmsConfiguration, + grantTokens, + //= aws-encryption-sdk-specification/framework/branch-key-store.md#aws-kms-branch-key-decryption + //# The operation MUST use the configured `KMS SDK Client` to decrypt the value of the branch key field. + kmsClient, + }: { + kmsClient: KMSClient + kmsConfiguration: Readonly + grantTokens?: ReadonlyArray + }, + encryptedHierarchicalKey: EncryptedHierarchicalKey +): Promise { + //= aws-encryption-sdk-specification/framework/branch-key-store.md#discovery + //# The Keystore MAY use the KMS Key ARNs already + //# persisted to the backing DynamoDB table, + //# provided they are in records created + //# with an identical Logical Keystore Name. + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#mrdiscovery + //# The Keystore MAY use the KMS Key ARNs already + //# persisted to the backing DynamoDB table, + //# provided they are in records created + //# with an identical Logical Keystore Name. + + const KeyId = kmsConfiguration.getCompatibleArnArn( + encryptedHierarchicalKey.kmsArn + ) + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#aws-kms-branch-key-decryption + //# When calling [AWS KMS Decrypt](https://docs.aws.amazon.com/kms/latest/APIReference/API_Decrypt.html), + //# the keystore operation MUST call with a request constructed as follows: + const response = await kmsClient.send( + new DecryptCommand({ + KeyId, + //= aws-encryption-sdk-specification/framework/branch-key-store.md#aws-kms-branch-key-decryption + //# - `CiphertextBlob` MUST be the `CiphertextBlob` attribute value on the provided EncryptedHierarchicalKey + CiphertextBlob: encryptedHierarchicalKey.ciphertextBlob, + //= aws-encryption-sdk-specification/framework/branch-key-store.md#aws-kms-branch-key-decryption + //# - `EncryptionContext` MUST be the [encryption context](#encryption-context) of the provided EncryptedHierarchicalKey + EncryptionContext: encryptedHierarchicalKey.encryptionContext, + //= aws-encryption-sdk-specification/framework/branch-key-store.md#aws-kms-branch-key-decryption + //# - `GrantTokens` MUST be this keystore's [grant tokens](https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#grant_token). + GrantTokens: grantTokens ? grantTokens.slice() : grantTokens, + }) + ) + + // error out if for some reason the KMS response does not contain the + // plaintext branch data key + needs(response.Plaintext, 'KMS branch key decryption failed') + // convert the unencrypted branch key into a Node Buffer + return Buffer.from(response.Plaintext as Uint8Array) +} + +/** + * This utility function constructs branch key materials from the authenticated + * encryption context + * @param branchKey + * @param branchKeyId + * @param authenticatedEncryptionContext + * @returns branch key materials + * @throws 'Unable to get branch key version to construct branch key materials from authenticated encryption context' + * if the type in the EC is invalid + */ +export function constructBranchKeyMaterials( + branchKey: Buffer, + encryptedHierarchicalKey: EncryptedHierarchicalKey +): NodeBranchKeyMaterial { + return new NodeBranchKeyMaterial( + //= aws-encryption-sdk-specification/framework/branch-key-store.md#branch-key-materials-from-authenticated-encryption-context + //# - [Branch Key](./structures.md#branch-key) MUST be the [decrypted branch key material](#aws-kms-branch-key-decryption) + branchKey, + //= aws-encryption-sdk-specification/framework/branch-key-store.md#branch-key-materials-from-authenticated-encryption-context + //# - [Branch Key Id](./structures.md#branch-key-id) MUST be the `branch-key-id` + encryptedHierarchicalKey.branchKeyId, + //= aws-encryption-sdk-specification/framework/branch-key-store.md#branch-key-materials-from-authenticated-encryption-context + //# - [Branch Key Version](./structures.md#branch-key-version) + //# The version string MUST start with `branch:version:`. + //# The remaining string encoded as UTF8 bytes MUST be the Branch Key version. + encryptedHierarchicalKey.type.version, + //= aws-encryption-sdk-specification/framework/branch-key-store.md#branch-key-materials-from-authenticated-encryption-context + //# - [Encryption Context](./structures.md#encryption-context-3) MUST be constructed by + //# [Custom Encryption Context From Authenticated Encryption Context](#custom-encryption-context-from-authenticated-encryption-context) + constructCustomEncryptionContext(encryptedHierarchicalKey.encryptionContext) + ) +} + +/** + * This is a utility function that constructs a custom encryption context from + * an authenticated encryption context + * @param authenticatedEncryptionContext + * @returns custom encryption context + */ +function constructCustomEncryptionContext( + authenticatedEncryptionContext: EncryptionContext +) { + const customEncryptionContext: EncryptionContext = {} + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#custom-encryption-context-from-authenticated-encryption-context + //# For every key in the [encryption context](./structures.md#encryption-context-3) + //# the string `aws-crypto-ec:` + the UTF8 decode of this key + //# MUST exist as a key in the authenticated encryption context. + //# Also, the value in the [encryption context](./structures.md#encryption-context-3) for this key + //# MUST equal the value in the authenticated encryption context + //# for the constructed key. + for (const [key, value] of Object.entries(authenticatedEncryptionContext)) { + if (key.startsWith(CUSTOM_ENCRYPTION_CONTEXT_FIELD_PREFIX)) { + customEncryptionContext[key] = value + } + } + + return customEncryptionContext +} diff --git a/modules/branch-keystore-node/src/branch_keystore_structures.ts b/modules/branch-keystore-node/src/branch_keystore_structures.ts new file mode 100644 index 000000000..a03436273 --- /dev/null +++ b/modules/branch-keystore-node/src/branch_keystore_structures.ts @@ -0,0 +1,25 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + BRANCH_KEY_ACTIVE_VERSION_FIELD, + BRANCH_KEY_FIELD, + BRANCH_KEY_IDENTIFIER_FIELD, + HIERARCHY_VERSION_FIELD, + KEY_CREATE_TIME_FIELD, + KMS_FIELD, + TYPE_FIELD, +} from './constants' + +// a nicer (easier-to-understand) type alias +export type BranchKeyItem = Record + +export interface BranchKeyRecord { + [BRANCH_KEY_IDENTIFIER_FIELD]: string + [TYPE_FIELD]: string + [BRANCH_KEY_ACTIVE_VERSION_FIELD]?: string + [BRANCH_KEY_FIELD]: Uint8Array + [KMS_FIELD]: string + [KEY_CREATE_TIME_FIELD]: string + [HIERARCHY_VERSION_FIELD]: number +} diff --git a/modules/branch-keystore-node/src/constants.ts b/modules/branch-keystore-node/src/constants.ts new file mode 100644 index 000000000..a1cca3d4f --- /dev/null +++ b/modules/branch-keystore-node/src/constants.ts @@ -0,0 +1,27 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +export const PARTITION_KEY = 'branch-key-id' +export const SORT_KEY = 'type' +export const TABLE_FIELD = 'tablename' +export const CUSTOM_ENCRYPTION_CONTEXT_FIELD_PREFIX = 'aws-crypto-ec:' +export const BRANCH_KEY_IDENTIFIER_FIELD = PARTITION_KEY +export const TYPE_FIELD = SORT_KEY +export const KEY_CREATE_TIME_FIELD = 'create-time' +export const HIERARCHY_VERSION_FIELD = 'hierarchy-version' +export const KMS_FIELD = 'kms-arn' +export const BRANCH_KEY_FIELD = 'enc' +export const BRANCH_KEY_ACTIVE_VERSION_FIELD = 'version' +export const BRANCH_KEY_TYPE_PREFIX = 'branch:version:' +export const BRANCH_KEY_ACTIVE_TYPE = 'branch:ACTIVE' +export const BEACON_KEY_TYPE_VALUE = 'beacon:ACTIVE' +export const POTENTIAL_BRANCH_KEY_RECORD_FIELDS = [ + BRANCH_KEY_IDENTIFIER_FIELD, + TYPE_FIELD, + KEY_CREATE_TIME_FIELD, + HIERARCHY_VERSION_FIELD, + KMS_FIELD, + BRANCH_KEY_FIELD, + BRANCH_KEY_ACTIVE_VERSION_FIELD, +] +export const KMS_CLIENT_USER_AGENT = 'aws-kms-hierarchy' diff --git a/modules/branch-keystore-node/src/dynamodb_key_storage.ts b/modules/branch-keystore-node/src/dynamodb_key_storage.ts new file mode 100644 index 000000000..a7e6159a9 --- /dev/null +++ b/modules/branch-keystore-node/src/dynamodb_key_storage.ts @@ -0,0 +1,217 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + IBranchKeyStorage, + EncryptedHierarchicalKey, + ActiveHierarchicalSymmetricVersion, + HierarchicalSymmetricVersion, +} from './types' +import { DynamoDBClient } from '@aws-sdk/client-dynamodb' +import { + getBranchKeyItem, + validateBranchKeyRecord, + constructAuthenticatedEncryptionContext, +} from './branch_keystore_helpers' + +import { + BRANCH_KEY_ACTIVE_TYPE, + BRANCH_KEY_TYPE_PREFIX, + BRANCH_KEY_FIELD, +} from './constants' +import { + immutableClass, + needs, + readOnlyProperty, +} from '@aws-crypto/material-management' + +//= aws-encryption-sdk-specification/framework/key-store/dynamodb-key-storage.md#initialization +//= type=implication +//# The following inputs MUST be specified to create a Dynamodb Key Storage Interface: +//# - [DynamoDb Client](#dynamodb-client) +//# - [Table Name](#table-name) +//# - [Logical KeyStore Name](#logical-keystore-name) +export interface DynamoDBKeyStorageInput { + ddbTableName: string + logicalKeyStoreName: string + ddbClient: DynamoDBClient +} + +//= aws-encryption-sdk-specification/framework/key-store/dynamodb-key-storage.md#operations +//= type=implication +//# The Dynamodb Key Storage Interface MUST implement the [key storage interface](./key-storage.md#interface). +export class DynamoDBKeyStorage implements IBranchKeyStorage { + public declare readonly ddbTableName: string + public declare readonly logicalKeyStoreName: string + public declare readonly ddbClient: DynamoDBClient + + constructor({ + ddbTableName, + logicalKeyStoreName, + ddbClient, + }: DynamoDBKeyStorageInput) { + /* Precondition: DDB table name must be a string */ + needs(typeof ddbTableName === 'string', 'DDB table name must be a string') + //= aws-encryption-sdk-specification/framework/branch-key-store.md#table-name + //# The table name of the DynamoDb table that backs this Keystore. + needs(ddbTableName, 'DynamoDb table name required') + + needs( + typeof logicalKeyStoreName === 'string', + 'DDB table name must be a string' + ) + + /* Precondition: DDB client must be a DynamoDBClient */ + needs( + ddbClient instanceof DynamoDBClient, + 'DDB client must be a DynamoDBClient' + ) + + readOnlyProperty(this, 'ddbTableName', ddbTableName) + readOnlyProperty(this, 'ddbClient', ddbClient) + readOnlyProperty(this, 'logicalKeyStoreName', logicalKeyStoreName) + + // make this instance immutable + Object.freeze(this) + } + + public async getEncryptedActiveBranchKey( + branchKeyId: string + ): Promise { + //= aws-encryption-sdk-specification/framework/key-store/dynamodb-key-storage.md#getencryptedactivebranchkey + //# To get the active version for the branch key id from the keystore + //# this operation MUST call AWS DDB `GetItem` + //# using the `branch-key-id` as the Partition Key and `"branch:ACTIVE"` value as the Sort Key. + + // get the ddb response item using the partition & sort keys + const ddbBranchKeyItem = await getBranchKeyItem( + this, + branchKeyId, + BRANCH_KEY_ACTIVE_TYPE + ) + // validate and form the branch key record + + //= aws-encryption-sdk-specification/framework/key-store/dynamodb-key-storage.md#getencryptedactivebranchkey + //# If the record does not contain the defined fields, this operation MUST fail. + + //= aws-encryption-sdk-specification/framework/key-store/dynamodb-key-storage.md#getencryptedactivebranchkey + //# The AWS DDB response MUST contain the fields defined in the [branch keystore record format](#record-format). + const ddbBranchKeyRecord = validateBranchKeyRecord(ddbBranchKeyItem) + // construct an encryption context from the record + const authenticatedEncryptionContext = + constructAuthenticatedEncryptionContext(this, ddbBranchKeyRecord) + + const encrypted = new EncryptedHierarchicalKey( + authenticatedEncryptionContext, + ddbBranchKeyRecord[BRANCH_KEY_FIELD] + ) + + //= aws-encryption-sdk-specification/framework/key-store/dynamodb-key-storage.md#getencryptedactivebranchkey + //# The returned EncryptedHierarchicalKey MUST have the same identifier as the input. + needs(encrypted.branchKeyId == branchKeyId, 'Unexpected branch key id.') + + //= aws-encryption-sdk-specification/framework/key-store/dynamodb-key-storage.md#getencryptedactivebranchkey + //# The returned EncryptedHierarchicalKey MUST have a type of ActiveHierarchicalSymmetricVersion. + needs( + encrypted.type instanceof ActiveHierarchicalSymmetricVersion, + 'Unexpected type. Not an active record.' + ) + + return encrypted + } + + public async getEncryptedBranchKeyVersion( + branchKeyId: string, + branchKeyVersion: string + ): Promise { + //= aws-encryption-sdk-specification/framework/key-store/dynamodb-key-storage.md#getencryptedbranchkeyversion + //# To get a branch key from the keystore this operation MUST call AWS DDB `GetItem` + //# using the `branch-key-id` as the Partition Key and "branch:version:" + `branchKeyVersion` value as the Sort Key. + + // get the ddb response item using the partition & sort keys + const ddbBranchKeyItem = await getBranchKeyItem( + this, + branchKeyId, + BRANCH_KEY_TYPE_PREFIX + branchKeyVersion + ) + + //= aws-encryption-sdk-specification/framework/key-store/dynamodb-key-storage.md#getencryptedbranchkeyversion + //# If the record does not contain the defined fields, this operation MUST fail. + + //= aws-encryption-sdk-specification/framework/key-store/dynamodb-key-storage.md#getencryptedbranchkeyversion + //# The AWS DDB response MUST contain the fields defined in the [branch keystore record format](#record-format). + + // validate and form the branch key record + const ddbBranchKeyRecord = validateBranchKeyRecord(ddbBranchKeyItem) + // construct an encryption context from the record + const authenticatedEncryptionContext = + constructAuthenticatedEncryptionContext(this, ddbBranchKeyRecord) + + const encrypted = new EncryptedHierarchicalKey( + authenticatedEncryptionContext, + ddbBranchKeyRecord[BRANCH_KEY_FIELD] + ) + + //= aws-encryption-sdk-specification/framework/key-store/dynamodb-key-storage.md#getencryptedbranchkeyversion + //# The returned EncryptedHierarchicalKey MUST have the same identifier as the input. + needs(encrypted.branchKeyId == branchKeyId, 'Unexpected branch key id.') + + //= aws-encryption-sdk-specification/framework/key-store/dynamodb-key-storage.md#getencryptedbranchkeyversion + //# The returned EncryptedHierarchicalKey MUST have the same version as the input. + needs( + encrypted.type.version == branchKeyVersion, + 'Unexpected branch key version.' + ) + + //= aws-encryption-sdk-specification/framework/key-store/dynamodb-key-storage.md#getencryptedbranchkeyversion + //# The returned EncryptedHierarchicalKey MUST have a type of HierarchicalSymmetricVersion. + needs( + encrypted.type instanceof HierarchicalSymmetricVersion, + 'Unexpected type. Not an version record.' + ) + + return encrypted + } + + getKeyStorageInfo() { + return { + name: this.ddbTableName, + logicalName: this.logicalKeyStoreName, + } + } +} + +immutableClass(DynamoDBKeyStorage) + +// This is a limited release for JS only. +// The full Key Store operations are available +// in the AWS Cryptographic Material Providers library +// in various languages (Java, .Net, Python, Rust...) + +//= aws-encryption-sdk-specification/framework/key-store/dynamodb-key-storage.md#writenewencryptedbranchkey +//= type=exception +//# To add the branch keys and a beacon key to the keystore the +//# operation MUST call [Amazon DynamoDB API TransactWriteItems](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_TransactWriteItems.html). +//# The call to Amazon DynamoDB TransactWriteItems MUST use the configured Amazon DynamoDB Client to make the call. +//# The operation MUST call Amazon DynamoDB TransactWriteItems with a request constructed as follows: + +//= aws-encryption-sdk-specification/framework/key-store/dynamodb-key-storage.md#writenewencryptedbranchkey +//= type=exception +//# If DDB TransactWriteItems is successful, this operation MUST return a successful response containing no additional data. +//# Otherwise, this operation MUST yield an error. + +//= aws-encryption-sdk-specification/framework/key-store/dynamodb-key-storage.md#writenewencryptedbranchkeyversion +//= type=exception +//# To add the new branch key to the keystore, +//# the operation MUST call [Amazon DynamoDB API TransactWriteItems](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_TransactWriteItems.html). +//# The call to Amazon DynamoDB TransactWriteItems MUST use the configured Amazon DynamoDB Client to make the call. +//# The operation MUST call Amazon DynamoDB TransactWriteItems with a request constructed as follows: + +//= aws-encryption-sdk-specification/framework/key-store/dynamodb-key-storage.md#getencryptedbeaconkey +//= type=exception +//# To get a branch key from the keystore this operation MUST call AWS DDB `GetItem` +//# using the `branch-key-id` as the Partition Key and "beacon:ACTIVE" value as the Sort Key. +//# The AWS DDB response MUST contain the fields defined in the [branch keystore record format](#record-format). +//# The returned EncryptedHierarchicalKey MUST have the same identifier as the input. +//# The returned EncryptedHierarchicalKey MUST have a type of ActiveHierarchicalSymmetricBeacon. +//# If the record does not contain the defined fields, this operation MUST fail. diff --git a/modules/branch-keystore-node/src/index.ts b/modules/branch-keystore-node/src/index.ts new file mode 100644 index 000000000..90aaed46d --- /dev/null +++ b/modules/branch-keystore-node/src/index.ts @@ -0,0 +1,5 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +export * from './kms_config' +export * from './branch_keystore' diff --git a/modules/branch-keystore-node/src/kms_config.ts b/modules/branch-keystore-node/src/kms_config.ts new file mode 100644 index 000000000..2004f9a26 --- /dev/null +++ b/modules/branch-keystore-node/src/kms_config.ts @@ -0,0 +1,228 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + // getRegionFromIdentifier, + parseAwsKmsKeyArn, +} from '@aws-crypto/kms-keyring' +import { + constructArnInOtherRegion, + mrkAwareAwsKmsKeyIdCompare, + ParsedAwsKmsKeyArn, +} from '@aws-crypto/kms-keyring/src/arn_parsing' +import { needs, readOnlyProperty } from '@aws-crypto/material-management' + +//= aws-encryption-sdk-specification/framework/branch-key-store.md#aws-kms-configuration +//# Both `KMS Key ARN` and `KMS MRKey ARN` accept MRK or regular Single Region KMS ARNs. +export interface KMSSingleRegionKey { + identifier: string +} +export interface KMSMultiRegionKey { + mrkIdentifier: string +} + +//= aws-encryption-sdk-specification/framework/branch-key-store.md#aws-kms-configuration +//# `Discovery` does not take an additional argument. +export type Discovery = 'discovery' + +//= aws-encryption-sdk-specification/framework/branch-key-store.md#aws-kms-configuration +//= type=implication +//# `MRDiscovery` MUST take an additional argument, which is a region. +export interface MrDiscovery { + region: string +} + +//= aws-encryption-sdk-specification/framework/branch-key-store.md#aws-kms-configuration +//# This configures the Keystore's KMS Key ARN restrictions, +//# which determines which KMS Key(s) is used +//# to wrap and unwrap the keys stored in Amazon DynamoDB. +//# There are four (4) options: +//# +//# - Discovery +//# - MRDiscovery +//# - Single Region Key Compatibility, denoted as `KMS Key ARN` +//# - Multi Region Key Compatibility, denoted as `KMS MRKey ARN` +export type KmsConfig = + | KMSSingleRegionKey + | KMSMultiRegionKey + | Discovery + | MrDiscovery + +// an interface to outline the common operations any of the 3 region-based AWS KMS +// configurations should perform +export interface RegionalKmsConfig { + /** + * this method tells the user the config's region + * @returns the region + */ + getRegion(): string + + /** + * this method tells the user if the config is compatible with an arn + * @param otherArn + * @returns a flag answering the method's purpose + */ + isCompatibleWithArn(otherArn: string): boolean +} + +// an abstract class defining common behavior for operations that SRK and MRK compatibility +// configs should perform +export class KmsKeyConfig implements RegionalKmsConfig { + public declare readonly _parsedArn: ParsedAwsKmsKeyArn + public declare readonly _arn: string + public declare readonly _mrkRegion: string + public declare readonly _config: KmsConfig + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#aws-kms-configuration + //# `KMS Key ARN` and `KMS MRKey ARN` MUST take an additional argument + //# that is a KMS ARN. + constructor(config: KmsConfig) { + readOnlyProperty(this, '_config', config) + if (config === 'discovery') { + // Nothing to set + } else if ('identifier' in config || 'mrkIdentifier' in config) { + const arn = + 'identifier' in config ? config.identifier : config.mrkIdentifier + /* Precondition: ARN must be a string */ + needs(arn || typeof arn === 'string', 'ARN must be a string') + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#aws-kms-configuration + //# To be clear, an KMS ARN for a Multi-Region Key MAY be provided to the `KMS Key ARN` configuration, + //# and a KMS ARN for non Multi-Region Key MAY be provided to the `KMS MRKey ARN` configuration. + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#aws-kms-configuration + //# This ARN MUST NOT be an Alias. + //# This ARN MUST be a valid + //# [AWS KMS Key ARN](./aws-kms/aws-kms-key-arn.md#a-valid-aws-kms-arn). + const parsedArn = parseAwsKmsKeyArn(arn) + needs( + parsedArn && parsedArn.ResourceType === 'key', + `${arn} must be a well-formed AWS KMS non-alias resource arn` + ) + + readOnlyProperty(this, '_parsedArn', parsedArn) + } else if ('region' in config) { + readOnlyProperty(this, '_mrkRegion', config.region) + } else { + needs(false, 'Unexpected config shape') + } + + Object.freeze(this) + } + + getRegion(): string { + if (this._config === 'discovery') { + //= aws-encryption-sdk-specification/framework/branch-key-store.md#initialization + //# If a DDB client needs to be constructed and the AWS KMS Configuration is Discovery, + //# a new DynamoDb client MUST be created with the default configuration. + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#initialization + //# If AWS KMS client needs to be constructed and the AWS KMS Configuration is Discovery, + //# a new AWS KMS client MUST be created with the default configuration. + return '' + } else if ( + 'identifier' in this._config || + 'mrkIdentifier' in this._config + ) { + //= aws-encryption-sdk-specification/framework/branch-key-store.md#initialization + //# If a DDB client needs to be constructed and the AWS KMS Configuration is KMS Key ARN or KMS MRKey ARN, + //# a new DynamoDb client MUST be created with the region of the supplied KMS ARN. + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#initialization + //# If AWS KMS client needs to be constructed and the AWS KMS Configuration is KMS Key ARN or KMS MRKey ARN, + //# a new AWS KMS client MUST be created with the region of the supplied KMS ARN. + return this._parsedArn.Region + } else if ('region' in this._config) { + //= aws-encryption-sdk-specification/framework/branch-key-store.md#initialization + //# If a DDB client needs to be constructed and the AWS KMS Configuration is MRDiscovery, + //# a new DynamoDb client MUST be created with the region configured in the MRDiscovery. + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#initialization + //# If AWS KMS client needs to be constructed and the AWS KMS Configuration is MRDiscovery, + //# a new AWS KMS client MUST be created with the region configured in the MRDiscovery. + return this._mrkRegion + } else { + needs(false, 'Unexpected configuration state') + } + } + + isCompatibleWithArn(otherArn: string): boolean { + if (this._config === 'discovery' || 'region' in this._config) { + // This may result in the function being called twice. + // However this is the most correct behavior + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#aws-kms-branch-key-decryption + //# If the Keystore's [AWS KMS Configuration](#aws-kms-configuration) is `Discovery` or `MRDiscovery`, + //# the `kms-arn` field of DDB response item MUST NOT be an Alias + //# or the operation MUST fail. + const parsedArn = parseAwsKmsKeyArn(otherArn) + needs( + parsedArn && parsedArn.ResourceType === 'key', + `${otherArn} must be a well-formed AWS KMS non-alias resource arn` + ) + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#aws-key-arn-compatibility + //# If the [AWS KMS Configuration](#aws-kms-configuration) is Discovery or MRDiscovery, + //# no comparison is ever made between ARNs. + return true + } else if ('identifier' in this._config) { + //= aws-encryption-sdk-specification/framework/branch-key-store.md#aws-key-arn-compatibility + //# For two ARNs to be compatible: + //# If the [AWS KMS Configuration](#aws-kms-configuration) designates single region ARN compatibility, + //# then two ARNs are compatible if they are exactly equal. + return this._arn == otherArn + } else if ('mrkIdentifier' in this._config) { + //= aws-encryption-sdk-specification/framework/branch-key-store.md#aws-key-arn-compatibility + //# If the [AWS KMS Configuration](#aws-kms-configuration) designates MRK ARN compatibility, + //# then two ARNs are compatible if they are equal in all parts other than the region. + //# That is, they are compatible if [AWS KMS MRK Match for Decrypt](aws-kms/aws-kms-mrk-match-for-decrypt.md#implementation) returns true. + return mrkAwareAwsKmsKeyIdCompare(this._arn, otherArn) + } else { + needs(false, 'Unexpected configuration state') + } + } + + getCompatibleArnArn(otherArn: string): string { + //= aws-encryption-sdk-specification/framework/branch-key-store.md#aws-kms-branch-key-decryption + //# If the Keystore's [AWS KMS Configuration](#aws-kms-configuration) is `KMS Key ARN` or `KMS MRKey ARN`, + //# the `kms-arn` field of the DDB response item MUST be + //# [compatible with](#aws-key-arn-compatibility) the configured KMS Key in + //# the [AWS KMS Configuration](#aws-kms-configuration) for this keystore, + //# or the operation MUST fail. + + //# If the Keystore's [AWS KMS Configuration](#aws-kms-configuration) is `Discovery` or `MRDiscovery`, + //# the `kms-arn` field of DDB response item MUST NOT be an Alias + //# or the operation MUST fail. + needs( + this.isCompatibleWithArn(otherArn), + 'KMS ARN from DDB response item MUST be compatible with the configured KMS Key in the AWS KMS Configuration for this keystore' + ) + + if (this._config == 'discovery') { + //= aws-encryption-sdk-specification/framework/branch-key-store.md#aws-kms-branch-key-decryption + //# - `KeyId`, if the KMS Configuration is Discovery, MUST be the `kms-arn` attribute value of the AWS DDB response item. + return otherArn + } else if ('region' in this._config) { + //= aws-encryption-sdk-specification/framework/branch-key-store.md#aws-kms-branch-key-decryption + //# If the KMS Configuration is MRDiscovery, `KeyId` MUST be the `kms-arn` attribute value of the AWS DDB response item, with the region replaced by the configured region. + const parsedArn = parseAwsKmsKeyArn(otherArn) + needs(parsedArn, 'KMS ARN from the keystore is not an ARN:' + otherArn) + return constructArnInOtherRegion(parsedArn, this._mrkRegion) + } else if ( + 'identifier' in this._config || + 'mrkIdentifier' in this._config + ) { + //= aws-encryption-sdk-specification/framework/branch-key-store.md#aws-kms-branch-key-decryption + //# Otherwise, it MUST BE the Keystore's configured KMS Key. + + // In this case, the equality condition has already been satisfied. + // In the SRK case this is strict equality, + // in the MKR case this is functional (everything but region) + return this._arn + } else { + // This is for completeness. + // It should should be impossible to get here. + needs(false, 'Unexpected configuration state') + } + } +} diff --git a/modules/branch-keystore-node/src/types.ts b/modules/branch-keystore-node/src/types.ts new file mode 100644 index 000000000..d46984262 --- /dev/null +++ b/modules/branch-keystore-node/src/types.ts @@ -0,0 +1,296 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { DynamoDBClient } from '@aws-sdk/client-dynamodb' +import { KMSClient } from '@aws-sdk/client-kms' +import { + needs, + immutableClass, + readOnlyProperty, +} from '@aws-crypto/material-management' +import { + BRANCH_KEY_TYPE_PREFIX, + BRANCH_KEY_IDENTIFIER_FIELD, + TABLE_FIELD, + TYPE_FIELD, + KEY_CREATE_TIME_FIELD, + HIERARCHY_VERSION_FIELD, + KMS_FIELD, + BRANCH_KEY_ACTIVE_VERSION_FIELD, + BRANCH_KEY_ACTIVE_TYPE, +} from './constants' +import { KmsConfig } from './kms_config' + +export type BranchKeyVersionType = `${typeof BRANCH_KEY_TYPE_PREFIX}${string}` +export type ActiveKeyEncryptionContext = { + [BRANCH_KEY_IDENTIFIER_FIELD]: string + [TABLE_FIELD]: string + [TYPE_FIELD]: typeof BRANCH_KEY_ACTIVE_TYPE + [KEY_CREATE_TIME_FIELD]: string + [HIERARCHY_VERSION_FIELD]: string + [KMS_FIELD]: string + [BRANCH_KEY_ACTIVE_VERSION_FIELD]: BranchKeyVersionType + [index: string]: string +} +export type VersionKeyEncryptionContext = { + [BRANCH_KEY_IDENTIFIER_FIELD]: string + [TABLE_FIELD]: string + [TYPE_FIELD]: BranchKeyVersionType + [KEY_CREATE_TIME_FIELD]: string + [HIERARCHY_VERSION_FIELD]: string + [KMS_FIELD]: string + [index: string]: string +} +export type BranchKeyEncryptionContext = + | ActiveKeyEncryptionContext + | VersionKeyEncryptionContext + +//= aws-encryption-sdk-specification/framework/key-store/key-storage.md#activehierarchicalsymmetric +//= type=implication +//# A structure that MUST have one member, +//# the UTF8 Encoded value of the version of the branch key. +export class ActiveHierarchicalSymmetricVersion { + public declare readonly version: string + + constructor(activeVersion: BranchKeyVersionType) { + //= aws-encryption-sdk-specification/framework/branch-key-store.md#branch-key-materials-from-authenticated-encryption-context + //# If the `type` attribute start with `"branch:version:"` then the version string MUST be equal to this value. + needs( + activeVersion.startsWith(BRANCH_KEY_TYPE_PREFIX), + 'Unexpected branch key type.' + ) + readOnlyProperty( + this, + 'version', + activeVersion.substring(BRANCH_KEY_TYPE_PREFIX.length) + ) + + Object.freeze(this) + } +} +immutableClass(ActiveHierarchicalSymmetricVersion) + +//= aws-encryption-sdk-specification/framework/key-store/key-storage.md#hierarchicalsymmetric +//= type=implication +//# A structure that MUST have one member, +//# the UTF8 Encoded value of the version of the branch key. +export class HierarchicalSymmetricVersion { + public declare readonly version: string + + constructor(type_field: BranchKeyVersionType) { + needs(type_field.startsWith(BRANCH_KEY_TYPE_PREFIX), '') + readOnlyProperty( + this, + 'version', + type_field.substring(BRANCH_KEY_TYPE_PREFIX.length) + ) + Object.freeze(this) + } +} +immutableClass(HierarchicalSymmetricVersion) + +//= aws-encryption-sdk-specification/framework/key-store/key-storage.md#type +//= type=implication +//# A union that MUST hold the following three options +//# - ActiveHierarchicalSymmetricVersion [ActiveHierarchicalSymmetric](#activehierarchicalsymmetric) +//# - HierarchicalSymmetricVersion [HierarchicalSymmetric](#hierarchicalsymmetric) +//# - ActiveHierarchicalSymmetricBeacon + +export type Type = + | ActiveHierarchicalSymmetricVersion + | HierarchicalSymmetricVersion + +export class EncryptedHierarchicalKey { + //= aws-encryption-sdk-specification/framework/key-store/key-storage.md#encryptedhierarchicalkey + //= type=implication + //# This structure MUST include all of the following fields: + //# - [BranchKeyId](./structures.md#branch-key-id) + //# - [Type](#type) + //# - CreateTime: Timestamp in ISO 8601 format in UTC, to microsecond precision. + //# - KmsArn: The AWS KMS Key ARN used to protect the CiphertextBlob value. + //# - [EncryptionContext](./structures.md#encryption-context-3) + //# - CiphertextBlob: The encrypted binary for the hierarchical key. + public declare readonly branchKeyId: string + public declare readonly type: Type + public declare readonly createTime: string + public declare readonly kmsArn: string + public declare readonly encryptionContext: + | ActiveKeyEncryptionContext + | VersionKeyEncryptionContext + public declare readonly ciphertextBlob: Uint8Array + + constructor( + encryptionContext: ActiveKeyEncryptionContext | VersionKeyEncryptionContext, + ciphertextBlob: Uint8Array + ) { + readOnlyProperty( + this, + 'branchKeyId', + encryptionContext[BRANCH_KEY_IDENTIFIER_FIELD] + ) + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#branch-key-materials-from-authenticated-encryption-context + //# The `type` attribute MUST either be equal to `"branch:ACTIVE"` or start with `"branch:version:"`. + needs( + encryptionContext[TYPE_FIELD] == BRANCH_KEY_ACTIVE_TYPE || + encryptionContext[TYPE_FIELD].startsWith(BRANCH_KEY_TYPE_PREFIX), + 'Unexpected branch key type.' + ) + + readOnlyProperty( + this, + 'type', + encryptionContext[TYPE_FIELD] == BRANCH_KEY_ACTIVE_TYPE + ? new ActiveHierarchicalSymmetricVersion( + //= aws-encryption-sdk-specification/framework/branch-key-store.md#branch-key-materials-from-authenticated-encryption-context + //# If the `type` attribute is equal to `"branch:ACTIVE"` + //# then the authenticated encryption context MUST have a `version` attribute + //# and the version string is this value. + encryptionContext[BRANCH_KEY_ACTIVE_VERSION_FIELD] + ) + : new HierarchicalSymmetricVersion(encryptionContext[TYPE_FIELD]) + ) + readOnlyProperty( + this, + 'createTime', + encryptionContext[KEY_CREATE_TIME_FIELD] + ) + readOnlyProperty(this, 'kmsArn', encryptionContext[KMS_FIELD]) + readOnlyProperty( + this, + 'encryptionContext', + Object.freeze({ ...encryptionContext }) + ) + readOnlyProperty(this, 'ciphertextBlob', ciphertextBlob) + + Object.freeze(this) + } +} +immutableClass(EncryptedHierarchicalKey) + +//= aws-encryption-sdk-specification/framework/key-store/key-storage.md#interface +//= type=implication +//# The KeyStorageInterface MUST support the following operations: +export interface IBranchKeyStorage { + //= aws-encryption-sdk-specification/framework/key-store/key-storage.md#interface + //= type=implication + //# - [GetEncryptedActiveBranchKey](#getencryptedactivebranchkey) + //# - [GetEncryptedBranchKeyVersion](#getencryptedbranchkeyversion) + + //= aws-encryption-sdk-specification/framework/key-store/key-storage.md#getencryptedactivebranchkey + //= type=implication + //# The GetEncryptedActiveBranchKey caller MUST provide the same inputs as the [GetActiveBranchKey](../branch-key-store.md#getactivebranchkey) operation. + + //= aws-encryption-sdk-specification/framework/key-store/key-storage.md#getencryptedactivebranchkey + //= type=implication + //# It MUST return an [EncryptedHierarchicalKey](#encryptedhierarchicalkey). + getEncryptedActiveBranchKey( + branchKeyId: string + ): Promise + + //= aws-encryption-sdk-specification/framework/key-store/key-storage.md#getencryptedbranchkeyversion + //= type=implication + //# The GetEncryptedBranchKeyVersion caller MUST provide the same inputs as the [GetBranchKeyVersion](../branch-key-store.md#getbranchkeyversion) operation. + + //= aws-encryption-sdk-specification/framework/key-store/key-storage.md#getencryptedbranchkeyversion + //= type=implication + //# It MUST return an [EncryptedHierarchicalKey](#encryptedhierarchicalkey). + getEncryptedBranchKeyVersion( + branchKeyId: string, + branchKeyVersion: string + ): Promise + + //= aws-encryption-sdk-specification/framework/key-store/key-storage.md#interface + //= type=implication + //# - [GetKeyStorageInfo](#getkeystorageinfo) + + //= aws-encryption-sdk-specification/framework/key-store/key-storage.md#getkeystorageinfo + //= type=implication + //# It MUST return the physical table name. + getKeyStorageInfo(): { name: string; logicalName: string } +} + +//= aws-encryption-sdk-specification/framework/key-store/key-storage.md#type +//= type=exception +//# - ActiveHierarchicalSymmetricBeacon + +export interface DynamoDBTable { + //= aws-encryption-sdk-specification/framework/branch-key-store.md#dynamodbtable + //= type=implication + //# A DynamoDBTable configuration MUST take the DynamoDB table name. + ddbTableName: string + //= aws-encryption-sdk-specification/framework/branch-key-store.md#dynamodbtable + //= type=implication + //# A DynamoDBTable configuration MAY take [DynamoDb Client](#dynamodb-client). + ddbClient?: DynamoDBClient +} + +//= aws-encryption-sdk-specification/framework/branch-key-store.md#storage +//# This configures how the Keystore will get encrypted data. +//# There are two valid storage options: +//# +//# - DynamoDBTable +//# - KeyStorage +export type Storage = DynamoDBTable | IBranchKeyStorage + +export interface AwsKms { + //= aws-encryption-sdk-specification/framework/branch-key-store.md#awskms + //= type=implication + //# An AwsKms configuration MAY take a list of AWS KMS [grant tokens](https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#grant_token). + grantTokens?: string[] + //= aws-encryption-sdk-specification/framework/branch-key-store.md#awskms + //= type=implication + //# An AwsKms configuration MAY take an [AWS KMS SDK client](#awskms). + kmsClient?: KMSClient +} + +export type KeyManagement = AwsKms + +//= aws-encryption-sdk-specification/framework/branch-key-store.md#initialization +//# The following inputs MAY be specified to create a KeyStore: +//# - [ID](#keystore-id) + +//= aws-encryption-sdk-specification/framework/branch-key-store.md#initialization +//# - [Storage](#storage) + +//= aws-encryption-sdk-specification/framework/branch-key-store.md#initialization +//# - [KeyManagement](#keymanagement) + +//= aws-encryption-sdk-specification/framework/branch-key-store.md#initialization +//# The following inputs MUST be specified to create a KeyStore: +//# - [AWS KMS Configuration](#aws-kms-configuration) +//# - [Logical KeyStore Name](#logical-keystore-name) +export interface BranchKeyStoreNodeInput { + logicalKeyStoreName: string + storage: Storage + keyManagement?: KeyManagement + kmsConfiguration: KmsConfig + keyStoreId?: string +} + +// This is a limited release for JS only. +// The full Key Store operations are available +// in the AWS Cryptographic Material Providers library +// in various languages (Java, .Net, Python, Rust...) + +//= aws-encryption-sdk-specification/framework/key-store/key-storage.md#interface +//= type=exception +//# - [WriteNewEncryptedBranchKey](#writenewencryptedbranchkey) +//# - [WriteNewEncryptedBranchKeyVersion](#writenewencryptedbranchkeyversion) + +//= aws-encryption-sdk-specification/framework/key-store/key-storage.md#interface +//= type=exception +//# - [GetEncryptedBeaconKey](#getencryptedbeaconkey) + +//= aws-encryption-sdk-specification/framework/key-store/key-storage.md#writenewencryptedbranchkey +//= type=exception +//# The WriteNewEncryptedBranchKey caller MUST provide: + +//= aws-encryption-sdk-specification/framework/key-store/key-storage.md#writenewencryptedbranchkeyversion +//= type=exception +//# The WriteNewEncryptedBranchKeyVersion caller MUST provide: + +//= aws-encryption-sdk-specification/framework/key-store/key-storage.md#getencryptedbeaconkey +//= type=exception +//# The GetEncryptedBeaconKey caller MUST provide the same inputs as the [GetBeaconKey](../branch-key-store.md#getbeaconkey) operation. +//# It MUST return an [EncryptedHierarchicalKey](#encryptedhierarchicalkey). diff --git a/modules/branch-keystore-node/test/branch_keystore.test.ts b/modules/branch-keystore-node/test/branch_keystore.test.ts new file mode 100644 index 000000000..0f7d9cd1f --- /dev/null +++ b/modules/branch-keystore-node/test/branch_keystore.test.ts @@ -0,0 +1,601 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import chai, { expect } from 'chai' +import { + BranchKeyStoreNode, + isIBranchKeyStoreNode, +} from '../src/branch_keystore' +import { DynamoDBKeyStorage } from '../src/dynamodb_key_storage' +import { validate, v4, version } from 'uuid' +import chaiAsPromised from 'chai-as-promised' +import { + KMSClient, + InvalidCiphertextException, + IncorrectKeyException, +} from '@aws-sdk/client-kms' +import { DynamoDBClient } from '@aws-sdk/client-dynamodb' +import { getRegionFromIdentifier } from '@aws-crypto/kms-keyring' +import { + BRANCH_KEY_ACTIVE_VERSION, + BRANCH_KEY_ACTIVE_VERSION_UTF8_BYTES, + BRANCH_KEY_ID, + DDB_TABLE_NAME, + INCORRECT_LOGICAL_NAME, + KEY_ARN, + KEY_ID, + KMS_KEY_ALIAS, + LOGICAL_KEYSTORE_NAME, + LYING_BRANCH_KEY_DECRYPT_ONLY_VERSION, + LYING_BRANCH_KEY_ID, + POSTAL_HORN_BRANCH_KEY_ID, + POSTAL_HORN_KEY_ARN, +} from './fixtures' +import { + BRANCH_KEY_ACTIVE_TYPE, + PARTITION_KEY, + SORT_KEY, +} from '../src/constants' + +chai.use(chaiAsPromised) +describe('Test Branch keystore', () => { + it('Test type guard', () => { + for (const keyStore of [null, undefined, 0, {}, '']) { + expect(isIBranchKeyStoreNode(keyStore as any)).to.be.false + } + }) + + describe('Test constructor', () => { + const KMS_CONFIGURATION = { identifier: KEY_ARN } + + const BRANCH_KEYSTORE = new BranchKeyStoreNode({ + storage: { ddbTableName: DDB_TABLE_NAME }, + logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, + kmsConfiguration: KMS_CONFIGURATION, + }) + + const falseyValues = [false, 0, -0, 0n, '', null, undefined, NaN] + const truthyValues = [[3, [], true], {}, 1, true, 'string'] + + it('Precondition: DDB table name must be a string', () => { + // all types of values except strings + const badVals = [...falseyValues, ...truthyValues].filter( + (v) => typeof v !== 'string' + ) + + for (const ddbTableName of badVals) { + expect( + () => + new BranchKeyStoreNode({ + storage: { ddbTableName: ddbTableName as any }, + logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, + kmsConfiguration: KMS_CONFIGURATION, + }) + ).to.throw('DDB table name must be a string') + } + }) + + it('Precondition: Logical keystore name must be a string', () => { + // all types of values except strings + const badVals = [...falseyValues, ...truthyValues].filter( + (v) => typeof v !== 'string' + ) + + for (const logicalKeyStoreName of badVals) { + expect( + () => + new BranchKeyStoreNode({ + storage: { ddbTableName: DDB_TABLE_NAME }, + logicalKeyStoreName: logicalKeyStoreName as any, + kmsConfiguration: KMS_CONFIGURATION, + }) + ).to.throw('Logical keystore name must be a string') + } + }) + + it('Precondition: KMS Configuration must be SRK', () => { + // all types of values + const badVals = [...falseyValues, ...truthyValues] + + for (const kmsConfiguration of badVals) { + expect( + () => + new BranchKeyStoreNode({ + storage: { ddbTableName: DDB_TABLE_NAME }, + logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, + kmsConfiguration: kmsConfiguration as any, + }) + ).to.throw('KMS Configuration must be SRK') + } + }) + + it('Precondition: KMS client must be a KMSClient', () => { + // only truthy values because KMS client may be falsey + for (const kmsClient of truthyValues) { + expect( + () => + new BranchKeyStoreNode({ + storage: { ddbTableName: DDB_TABLE_NAME }, + logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, + kmsConfiguration: KMS_CONFIGURATION, + keyManagement: { kmsClient: kmsClient as any }, + }) + ).to.throw('KMS client must be a KMSClient') + } + }) + + it('Precondition: DDB client must be a DynamoDBClient', () => { + // only truthy values because DDB client may be falsey + for (const ddbClient of truthyValues) { + expect( + () => + new BranchKeyStoreNode({ + storage: { + ddbTableName: DDB_TABLE_NAME, + ddbClient: ddbClient as any, + }, + logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, + kmsConfiguration: KMS_CONFIGURATION, + }) + ).to.throw('DDB client must be a DynamoDBClient') + } + }) + + it('Precondition: Keystore id must be a string', () => { + // only truthy values that are not strings because keystore id may be + // falsey + const badVals = truthyValues.filter((v) => typeof v !== 'string') + for (const keyStoreId of badVals) { + expect( + () => + new BranchKeyStoreNode({ + storage: { ddbTableName: DDB_TABLE_NAME }, + logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, + kmsConfiguration: KMS_CONFIGURATION, + keyStoreId: keyStoreId as any, + }) + ).to.throw('Keystore id must be a string') + } + }) + + it('Precondition: Grant tokens must be a string array', () => { + // use only truthy values because grantTokens may be falsey + for (const grantTokens of truthyValues) { + expect( + () => + new BranchKeyStoreNode({ + storage: { ddbTableName: DDB_TABLE_NAME }, + logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, + kmsConfiguration: KMS_CONFIGURATION, + keyManagement: { grantTokens: grantTokens as any }, + }) + ).to.throw('Grant tokens must be a string array') + } + }) + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#aws-kms-grant-tokens + //= type=test + //# A list of AWS KMS [grant tokens](https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#grant_token). + it('Postcondition: If unprovided, the grant tokens are undefined', () => { + for (const grantTokens of falseyValues) { + expect( + new BranchKeyStoreNode({ + storage: { ddbTableName: DDB_TABLE_NAME }, + logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, + kmsConfiguration: KMS_CONFIGURATION, + keyManagement: { grantTokens: grantTokens as any }, + }).grantTokens + ).to.equal(undefined) + } + }) + + it('Invalid KmsKeyArn config', () => { + const kmsClient = new KMSClient({}) + const ddbClient = new DynamoDBClient({}) + expect(() => { + const kmsConfig = { identifier: KEY_ID } + return new BranchKeyStoreNode({ + storage: { ddbTableName: DDB_TABLE_NAME, ddbClient }, + logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, + kmsConfiguration: kmsConfig, + keyManagement: { kmsClient }, + }) + }).to.throw( + `${KEY_ID} must be a well-formed AWS KMS non-alias resource arn` + ) + }) + + it('Invalid KmsKeyArn Alias config', () => { + const kmsClient = new KMSClient({}) + const ddbClient = new DynamoDBClient({}) + expect(() => { + const kmsConfig = { identifier: KMS_KEY_ALIAS } + return new BranchKeyStoreNode({ + storage: { ddbTableName: DDB_TABLE_NAME, ddbClient }, + logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, + kmsConfiguration: kmsConfig, + keyManagement: { kmsClient }, + }) + }).to.throw( + `${KMS_KEY_ALIAS} must be a well-formed AWS KMS non-alias resource arn` + ) + }) + + it('Valid config', () => { + const kmsClient = new KMSClient({}) + const ddbClient = new DynamoDBClient({}) + const kmsConfig = { identifier: KEY_ID } + const keyStore = new BranchKeyStoreNode({ + storage: { ddbTableName: DDB_TABLE_NAME, ddbClient }, + logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, + kmsConfiguration: kmsConfig, + keyManagement: { kmsClient }, + }) + + expect( + validate(keyStore.keyStoreId) && version(keyStore.keyStoreId) === 4 + ).equals(true) + // expect(keyStore.ddbTableName).equals(DDB_TABLE_NAME) + expect(keyStore.kmsConfiguration).equals(kmsConfig) + }) + + it('Test valid config with no clients', () => { + const kmsClient = new KMSClient({}) + const ddbClient = new DynamoDBClient({}) + const kmsConfig = { identifier: KEY_ID } + + // test with no kms client supplied + expect( + () => + new BranchKeyStoreNode({ + storage: { ddbTableName: DDB_TABLE_NAME, ddbClient }, + logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, + kmsConfiguration: kmsConfig, + }) + ).to.not.throw() + + // test with no ddb client supplied + expect( + () => + new BranchKeyStoreNode({ + storage: { ddbTableName: DDB_TABLE_NAME }, + logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, + kmsConfiguration: kmsConfig, + keyManagement: { kmsClient }, + }) + ).to.not.throw() + + // test with no clients supplied + expect( + () => + new BranchKeyStoreNode({ + storage: { ddbTableName: DDB_TABLE_NAME }, + logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, + kmsConfiguration: kmsConfig, + }) + ).to.not.throw() + }) + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#keystore-id + //= type=test + //# The Identifier for this KeyStore. + //# If one is not supplied, then a [version 4 UUID](https://www.ietf.org/rfc/rfc4122.txt) MUST be used. + it('Postcondition: If unprovided, the keystore id is a generated valid uuidv4', () => { + for (const keyStoreId of falseyValues) { + const { keyStoreId: id } = new BranchKeyStoreNode({ + storage: { ddbTableName: DDB_TABLE_NAME }, + logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, + kmsConfiguration: KMS_CONFIGURATION, + keyStoreId: keyStoreId as any, + }) + + expect(validate(id) && version(id) === 4).equals(true) + } + }) + + it('Postcondition: If unprovided, the DDB client is configured', async () => { + for (const ddbClient of falseyValues) { + const { storage } = new BranchKeyStoreNode({ + storage: { + ddbTableName: DDB_TABLE_NAME, + ddbClient: ddbClient as any, + }, + logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, + kmsConfiguration: KMS_CONFIGURATION, + }) + + expect(storage instanceof DynamoDBKeyStorage).to.equals(true) + expect( + await (storage as DynamoDBKeyStorage).ddbClient.config.region() + ).to.equal(getRegionFromIdentifier(KEY_ARN)) + } + }) + + it('Postcondition: If unprovided, the KMS client is configured', async () => { + for (const kmsClient of falseyValues) { + const { kmsClient: client } = new BranchKeyStoreNode({ + storage: { ddbTableName: DDB_TABLE_NAME }, + logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, + kmsConfiguration: KMS_CONFIGURATION, + keyManagement: { kmsClient: kmsClient as any }, + }) + + expect(await client.config.region()).to.equal( + getRegionFromIdentifier(KEY_ARN) + ) + } + }) + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#table-name + //= type=test + //# The table name of the DynamoDb table that backs this Keystore. + it('Null table name provided', () => { + expect( + () => + new BranchKeyStoreNode({ + storage: { ddbTableName: '' }, + logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, + kmsConfiguration: KMS_CONFIGURATION, + }) + ).to.throw('DynamoDb table name required') + }) + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#logical-keystore-name + //= type=test + //# This name is cryptographically bound to all data stored in this table, + //# and logically separates data between different tables. + //# The logical keystore name MUST be bound to every created key. + //# There needs to be a one to one mapping between DynamoDB Table Names and the Logical KeyStore Name. + //# This value can be set to the DynamoDB table name itself, but does not need to. + //# Controlling this value independently enables restoring from DDB table backups + //# even when the table name after restoration is not exactly the same. + it('Null logical keystore name provided', () => { + expect( + () => + new BranchKeyStoreNode({ + storage: { ddbTableName: DDB_TABLE_NAME }, + logicalKeyStoreName: '', + kmsConfiguration: KMS_CONFIGURATION, + }) + ).to.throw('Logical Keystore name required') + }) + + describe('Test proper init', () => { + it('KMS Configuration is immutable', () => { + expect(Object.isFrozen(BRANCH_KEYSTORE.kmsConfiguration)).equals(true) + }) + + it('Keystore is immutable', () => { + expect(Object.isFrozen(BRANCH_KEYSTORE)).equals(true) + }) + + it('Attributes are correct', () => { + const kmsClient = new KMSClient({ + region: getRegionFromIdentifier(KEY_ARN), + }) + const ddbClient = new DynamoDBClient({ + region: getRegionFromIdentifier(KEY_ARN), + }) + const keyStoreId = v4() + const grantTokens = [] as string[] + const test = new BranchKeyStoreNode({ + storage: { ddbTableName: DDB_TABLE_NAME, ddbClient: ddbClient }, + logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, + kmsConfiguration: KMS_CONFIGURATION, + keyStoreId: keyStoreId, + keyManagement: { kmsClient: kmsClient, grantTokens: grantTokens }, + }) + + expect((test.storage as DynamoDBKeyStorage).ddbTableName).to.equal( + DDB_TABLE_NAME + ) + expect(test.logicalKeyStoreName).to.equal(LOGICAL_KEYSTORE_NAME) + expect(test.kmsConfiguration).to.equal(KMS_CONFIGURATION) + expect(test.kmsClient).to.equal(kmsClient) + expect((test.storage as DynamoDBKeyStorage).ddbClient).to.equal( + ddbClient + ) + expect(test.keyStoreId).to.equal(keyStoreId) + expect(test.grantTokens).to.equal(grantTokens) + }) + }) + }) + + // the following tests are all integration tests. These tests test + // getActiveBranchKey and getBranchKeyVersion as a whole while making network + // calls to DDB and KMS + it('Test get active key', async () => { + const kmsClient = new KMSClient({}) + const ddbClient = new DynamoDBClient({}) + const kmsConfig = { identifier: KEY_ID } + const keyStore = new BranchKeyStoreNode({ + kmsConfiguration: kmsConfig, + storage: { ddbTableName: DDB_TABLE_NAME, ddbClient: ddbClient }, + logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, + + keyManagement: { kmsClient: kmsClient }, + }) + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#getactivebranchkey + //= type=test + //# On invocation, the caller: + //# - MUST supply a `branch-key-id` + await expect(keyStore.getActiveBranchKey('')).to.be.rejectedWith( + 'MUST supply a string branch key id' + ) + + // test type checks + await expect( + keyStore.getActiveBranchKey(undefined as any) + ).to.be.rejectedWith('MUST supply a string branch key id') + await expect(keyStore.getActiveBranchKey(null as any)).to.be.rejectedWith( + 'MUST supply a string branch key id' + ) + + const branchKeyMaterials = await keyStore.getActiveBranchKey(BRANCH_KEY_ID) + // expect(branchKeyMaterials.branchKeyIdentifier).equals(BRANCH_KEY_ID) + expect(branchKeyMaterials.branchKeyVersion).deep.equals( + BRANCH_KEY_ACTIVE_VERSION_UTF8_BYTES + ) + expect(branchKeyMaterials.branchKey().length).equals(32) + }) + + it('Test get branch key version', async () => { + const kmsClient = new KMSClient({}) + const ddbClient = new DynamoDBClient({}) + const kmsConfig = { identifier: KEY_ID } + + const keyStore = new BranchKeyStoreNode({ + kmsConfiguration: kmsConfig, + logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, + storage: { ddbTableName: DDB_TABLE_NAME, ddbClient: ddbClient }, + + keyManagement: { kmsClient: kmsClient }, + }) + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#getbranchkeyversion + //= type=test + //# On invocation, the caller: + //# - MUST supply a `branch-key-id` + //# - MUST supply a `branchKeyVersion` + await expect( + keyStore.getBranchKeyVersion('', BRANCH_KEY_ACTIVE_VERSION) + ).to.be.rejectedWith('MUST supply a string branch key id') + await expect( + keyStore.getBranchKeyVersion(BRANCH_KEY_ID, '') + ).to.be.rejectedWith('MUST supply a string branch key version') + + // test type checks + await expect( + keyStore.getBranchKeyVersion(undefined as any, BRANCH_KEY_ACTIVE_VERSION) + ).to.be.rejectedWith('MUST supply a string branch key id') + await expect( + keyStore.getBranchKeyVersion(null as any, BRANCH_KEY_ACTIVE_VERSION) + ).to.be.rejectedWith('MUST supply a string branch key id') + await expect( + keyStore.getBranchKeyVersion(BRANCH_KEY_ID, undefined as any) + ).to.be.rejectedWith('MUST supply a string branch key version') + await expect( + keyStore.getBranchKeyVersion(BRANCH_KEY_ID, null as any) + ).to.be.rejectedWith('MUST supply a string branch key version') + + const branchKeyMaterials = await keyStore.getBranchKeyVersion( + BRANCH_KEY_ID, + BRANCH_KEY_ACTIVE_VERSION + ) + expect(branchKeyMaterials.branchKeyIdentifier).equals(BRANCH_KEY_ID) + expect(branchKeyMaterials.branchKeyVersion).deep.equals( + BRANCH_KEY_ACTIVE_VERSION_UTF8_BYTES + ) + expect(branchKeyMaterials.branchKey().length).equals(32) + }) + + it('Test get active key with incorrect kms key arn', async () => { + const kmsClient = new KMSClient({}) + const ddbClient = new DynamoDBClient({}) + const kmsConfig = { identifier: KEY_ID } + + const keyStore = new BranchKeyStoreNode({ + kmsConfiguration: kmsConfig, + logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, + storage: { ddbTableName: DDB_TABLE_NAME, ddbClient }, + + keyManagement: { kmsClient }, + }) + + void (await expect( + keyStore.getActiveBranchKey(POSTAL_HORN_BRANCH_KEY_ID) + ).to.be.rejectedWith( + 'KMS ARN from DDB response item MUST be compatible with the configured KMS Key in the AWS KMS Configuration for this keystore' + )) + }) + + it('Test get active key with wrong logical keystore name', async () => { + const kmsClient = new KMSClient({}) + const ddbClient = new DynamoDBClient({}) + const kmsConfig = { identifier: KEY_ID } + + const keyStore = new BranchKeyStoreNode({ + kmsConfiguration: kmsConfig, + logicalKeyStoreName: INCORRECT_LOGICAL_NAME, + storage: { ddbTableName: DDB_TABLE_NAME, ddbClient }, + + keyManagement: { kmsClient }, + }) + + void (await expect( + keyStore.getActiveBranchKey(BRANCH_KEY_ID) + ).to.be.rejectedWith(InvalidCiphertextException)) + }) + + it('Test get active key does not exist fails', async () => { + const kmsClient = new KMSClient({}) + const ddbClient = new DynamoDBClient({}) + const kmsConfig = { identifier: KEY_ID } + + const keyStore = new BranchKeyStoreNode({ + kmsConfiguration: kmsConfig, + logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, + storage: { ddbTableName: DDB_TABLE_NAME, ddbClient }, + + keyManagement: { kmsClient }, + }) + + void (await expect( + keyStore.getActiveBranchKey('Robbie') + ).to.be.rejectedWith( + `A branch key record with ${PARTITION_KEY}=Robbie and ${SORT_KEY}=${BRANCH_KEY_ACTIVE_TYPE} was not found in DynamoDB` + )) + }) + + it('Test get active key with no clients', async () => { + const kmsConfig = { identifier: KEY_ID } + const keyStore = new BranchKeyStoreNode({ + kmsConfiguration: kmsConfig, + logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, + storage: { ddbTableName: DDB_TABLE_NAME }, + }) + + const branchKeyMaterials = await keyStore.getActiveBranchKey(BRANCH_KEY_ID) + expect(branchKeyMaterials.branchKey().length).equals(32) + }) + + it('Test get active key for lying branch key', async () => { + const kmsClient = new KMSClient({}) + const ddbClient = new DynamoDBClient({}) + const kmsConfig = { identifier: POSTAL_HORN_KEY_ARN } + + const keyStore = new BranchKeyStoreNode({ + kmsConfiguration: kmsConfig, + logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, + storage: { ddbTableName: DDB_TABLE_NAME, ddbClient }, + + keyManagement: { kmsClient }, + }) + + void (await expect( + keyStore.getActiveBranchKey(LYING_BRANCH_KEY_ID) + ).to.be.rejectedWith(IncorrectKeyException)) + }) + + it('Test get versioned key for lying branch key', async () => { + const kmsClient = new KMSClient({}) + const ddbClient = new DynamoDBClient({}) + const kmsConfig = { identifier: POSTAL_HORN_KEY_ARN } + + const keyStore = new BranchKeyStoreNode({ + kmsConfiguration: kmsConfig, + logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, + storage: { ddbTableName: DDB_TABLE_NAME, ddbClient }, + + keyManagement: { kmsClient }, + }) + + void (await expect( + keyStore.getBranchKeyVersion( + LYING_BRANCH_KEY_ID, + LYING_BRANCH_KEY_DECRYPT_ONLY_VERSION + ) + ).to.be.rejectedWith(IncorrectKeyException)) + }) +}) diff --git a/modules/branch-keystore-node/test/branch_keystore_helpers.test.ts b/modules/branch-keystore-node/test/branch_keystore_helpers.test.ts new file mode 100644 index 000000000..823ce3d0d --- /dev/null +++ b/modules/branch-keystore-node/test/branch_keystore_helpers.test.ts @@ -0,0 +1,686 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import chai, { expect } from 'chai' +import chaiAsPromised from 'chai-as-promised' +import { BranchKeyStoreNode } from '../src/branch_keystore' +import { + constructAuthenticatedEncryptionContext, + constructBranchKeyMaterials, + decryptBranchKey, + getBranchKeyItem, + validateBranchKeyRecord, +} from '../src/branch_keystore_helpers' +import { KMSClient } from '@aws-sdk/client-kms' +import { getRegionFromIdentifier } from '@aws-crypto/kms-keyring' +import { DecryptCommand } from '@aws-sdk/client-kms' +import { + BRANCH_KEY_ID, + DDB_TABLE_NAME, + KEY_ARN, + LOGICAL_KEYSTORE_NAME, + ACTIVE_BRANCH_KEY, + VERSION_BRANCH_KEY, + ENCRYPTED_ACTIVE_BRANCH_KEY, + ENCRYPTED_VERSION_BRANCH_KEY, +} from './fixtures' +import { BranchKeyItem } from '../src/branch_keystore_structures' +import { + BRANCH_KEY_ACTIVE_TYPE, + BRANCH_KEY_ACTIVE_VERSION_FIELD, + BRANCH_KEY_FIELD, + BRANCH_KEY_IDENTIFIER_FIELD, + BRANCH_KEY_TYPE_PREFIX, + CUSTOM_ENCRYPTION_CONTEXT_FIELD_PREFIX, + HIERARCHY_VERSION_FIELD, + KEY_CREATE_TIME_FIELD, + KMS_FIELD, + TYPE_FIELD, + PARTITION_KEY, + SORT_KEY, +} from '../src/constants' +import { DynamoDBKeyStorage } from '../src/dynamodb_key_storage' +import { + EncryptedHierarchicalKey, + // ActiveKeyEncryptionContext, + // VersionKeyEncryptionContext, + // BranchKeyVersionType, +} from '../src/types' + +const VALID_CUSTOM_ENCRYPTION_CONTEXT_KV_PAIRS = { + 'aws-crypto-ec:key1': 'value 1', + 'aws-crypto-ec:key2': 2, + 'aws-crypto-ec:key3': true, +} + +const VALID_CUSTOM_ENCRYPTION_CONTEXT = Object.fromEntries( + Object.entries({ ...VALID_CUSTOM_ENCRYPTION_CONTEXT_KV_PAIRS }).map( + ([key, value]) => [key, value.toString()] + ) +) + +const INVALID_CUSTOM_ENCRYPTION_CONTEXT_KV_PAIRS = { + 'awz-crypto-ec:key1': 'value 1', + key2: 'value 2', + 'aws-crypt0-ec:key3': 'value 3', +} + +const BRANCH_KEYSTORE = new BranchKeyStoreNode({ + storage: { ddbTableName: DDB_TABLE_NAME }, + logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, + kmsConfiguration: { identifier: KEY_ARN }, +}) + +const BRANCH_KEY_STORAGE = BRANCH_KEYSTORE.storage as DynamoDBKeyStorage + +chai.use(chaiAsPromised) +// TODO: Should we mock DDB and KMS client? +describe('Test keystore helpers', () => { + describe('Test getBranchKeyItem', () => { + it('Getting an active branch key', async () => { + const item = await getBranchKeyItem( + BRANCH_KEY_STORAGE, + BRANCH_KEY_ID, + BRANCH_KEY_ACTIVE_TYPE + ) + + expect( + item && + TYPE_FIELD in item && + item[TYPE_FIELD] == BRANCH_KEY_ACTIVE_TYPE && + BRANCH_KEY_ACTIVE_VERSION_FIELD in item && + item[BRANCH_KEY_ACTIVE_VERSION_FIELD].startsWith( + BRANCH_KEY_TYPE_PREFIX + ) + ).equals(true) + }) + + it('Getting a versioned branch key', async () => { + const item = await getBranchKeyItem( + BRANCH_KEY_STORAGE, + BRANCH_KEY_ID, + ENCRYPTED_VERSION_BRANCH_KEY.encryptionContext[TYPE_FIELD] + ) + + expect( + item && + !(BRANCH_KEY_ACTIVE_VERSION_FIELD in item) && + item[TYPE_FIELD].startsWith(BRANCH_KEY_TYPE_PREFIX) + ).equals(true) + }) + + it('Getting an active & versioned branch key via a nonexistent branch key id', async () => { + const nonexistentBranchKeyId = BRANCH_KEY_ID.replace('8', '7') + + void (await expect( + getBranchKeyItem( + BRANCH_KEY_STORAGE, + nonexistentBranchKeyId, + BRANCH_KEY_ACTIVE_TYPE + ) + ).to.rejectedWith( + `A branch key record with ${PARTITION_KEY}=${nonexistentBranchKeyId} and ${SORT_KEY}=${BRANCH_KEY_ACTIVE_TYPE} was not found in DynamoDB` + )) + + void (await expect( + getBranchKeyItem( + BRANCH_KEY_STORAGE, + nonexistentBranchKeyId, + VERSION_BRANCH_KEY[TYPE_FIELD] + ) + ).to.be.rejectedWith( + `A branch key record with ${PARTITION_KEY}=${nonexistentBranchKeyId} and ${SORT_KEY}=${VERSION_BRANCH_KEY[TYPE_FIELD]} was not found in DynamoDB` + )) + }) + + it('Getting a versioned branch key via a nonexistent version', async () => { + const type = VERSION_BRANCH_KEY[TYPE_FIELD] + const nonexistentType = type.replace('0', '1') + + void (await expect( + getBranchKeyItem(BRANCH_KEY_STORAGE, BRANCH_KEY_ID, nonexistentType) + ).to.be.rejectedWith( + `A branch key record with ${PARTITION_KEY}=${BRANCH_KEY_ID} and ${SORT_KEY}=${nonexistentType} was not found in DynamoDB` + )) + }) + }) + + describe('Test validateBranchKeyRecord', () => { + it('With valid active & versioned branch key items', () => { + expect(validateBranchKeyRecord(ACTIVE_BRANCH_KEY)).to.deep.equals( + ACTIVE_BRANCH_KEY + ) + expect(validateBranchKeyRecord(VERSION_BRANCH_KEY)).to.deep.equals( + VERSION_BRANCH_KEY + ) + }) + + it('With valid active & versioned items bearing extra keys prefixed properly', () => { + const activeItem = { + ...ACTIVE_BRANCH_KEY, + ...VALID_CUSTOM_ENCRYPTION_CONTEXT_KV_PAIRS, + } + expect(validateBranchKeyRecord(activeItem)).to.deep.equals({ + ...ACTIVE_BRANCH_KEY, + ...VALID_CUSTOM_ENCRYPTION_CONTEXT_KV_PAIRS, + }) + + const versionItem = { + ...VERSION_BRANCH_KEY, + ...VALID_CUSTOM_ENCRYPTION_CONTEXT_KV_PAIRS, + } + expect(validateBranchKeyRecord(versionItem)).to.deep.equals({ + ...VERSION_BRANCH_KEY, + ...VALID_CUSTOM_ENCRYPTION_CONTEXT_KV_PAIRS, + }) + }) + + // = aws-encryption-sdk-specification/framework/branch-key-store.md#record-format + // = type=test + // # 1. `branch-key-id` : Unique identifier for a branch key; represented as [AWS DDB String](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes) + it(`Active & versioned items have no ${BRANCH_KEY_IDENTIFIER_FIELD} field`, () => { + const activeItem: BranchKeyItem = { ...ACTIVE_BRANCH_KEY } + delete activeItem[BRANCH_KEY_IDENTIFIER_FIELD] + expect(() => validateBranchKeyRecord(activeItem)).to.throw( + `Branch keystore record does not contain a ${BRANCH_KEY_IDENTIFIER_FIELD} field of type string` + ) + + const versionedItem: BranchKeyItem = { ...VERSION_BRANCH_KEY } + delete versionedItem[BRANCH_KEY_IDENTIFIER_FIELD] + expect(() => validateBranchKeyRecord(versionedItem)).to.throw( + `Branch keystore record does not contain a ${BRANCH_KEY_IDENTIFIER_FIELD} field of type string` + ) + }) + + // = aws-encryption-sdk-specification/framework/branch-key-store.md#record-format + // = type=test + // # 1. `type` : One of the following; represented as [AWS DDB String](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes) + // # - The string literal `"beacon:ACTIVE"`. Then `enc` is the wrapped beacon key. + // # - The string `"branch:version:"` + `version`, where `version` is the Branch Key Version. Then `enc` is the wrapped branch key. + // # - The string literal `"branch:ACTIVE"`. Then `enc` is the wrapped beacon key of the active version. + it(`Active & versioned items have no ${TYPE_FIELD} field`, () => { + const activeItem: BranchKeyItem = { ...ACTIVE_BRANCH_KEY } + delete activeItem[TYPE_FIELD] + expect(() => validateBranchKeyRecord(activeItem)).to.throw( + `Branch keystore record does not contain a valid ${TYPE_FIELD} field of type string` + ) + + const versionedItem: BranchKeyItem = { ...VERSION_BRANCH_KEY } + delete versionedItem[TYPE_FIELD] + expect(() => validateBranchKeyRecord(versionedItem)).to.throw( + `Branch keystore record does not contain a valid ${TYPE_FIELD} field of type string` + ) + }) + + it(`Versioned branch key item has an improper ${TYPE_FIELD} field`, () => { + const item = { ...VERSION_BRANCH_KEY } + item[TYPE_FIELD] = item[TYPE_FIELD].substring( + BRANCH_KEY_TYPE_PREFIX.length + ) + expect(() => validateBranchKeyRecord(item)).to.throw( + `Branch keystore record does not contain a valid ${TYPE_FIELD} field of type string` + ) + }) + + it('Item type is none of 3 possible types (branch:ACTIVE, starting with branch:version:, or beacon:ACTIVE)', () => { + const item = { ...ACTIVE_BRANCH_KEY } + item[TYPE_FIELD] = 'lol' + expect(() => validateBranchKeyRecord(item)).to.throw( + `Branch keystore record does not contain a valid ${TYPE_FIELD} field of type string` + ) + }) + + // = aws-encryption-sdk-specification/framework/branch-key-store.md#record-format + // = type=test + // # 1. `version` : Only exists if `type` is the string literal `"branch:ACTIVE"`. + // # Then it is the Branch Key Version. represented as [AWS DDB String](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes) + it(`Active branch key item has no ${BRANCH_KEY_ACTIVE_VERSION_FIELD} field`, () => { + const item: BranchKeyItem = { ...ACTIVE_BRANCH_KEY } + delete item[BRANCH_KEY_ACTIVE_VERSION_FIELD] + expect(() => validateBranchKeyRecord(item)).to.throw( + `Branch keystore record does not contain a ${BRANCH_KEY_ACTIVE_VERSION_FIELD} field of type string` + ) + }) + + // = aws-encryption-sdk-specification/framework/branch-key-store.md#record-format + // = type=test + // # 1. `enc` : Encrypted version of the key; + // # represented as [AWS DDB Binary](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes) + it(`Active & versioned items have no ${BRANCH_KEY_FIELD} field`, () => { + const activeItem: BranchKeyItem = { ...ACTIVE_BRANCH_KEY } + delete activeItem[BRANCH_KEY_FIELD] + expect(() => validateBranchKeyRecord(activeItem)).to.throw( + `Branch keystore record does not contain ${BRANCH_KEY_FIELD} field of type Uint8Array` + ) + + const versionedItem: BranchKeyItem = { ...VERSION_BRANCH_KEY } + delete versionedItem[BRANCH_KEY_FIELD] + expect(() => validateBranchKeyRecord(versionedItem)).to.throw( + `Branch keystore record does not contain ${BRANCH_KEY_FIELD} field of type Uint8Array` + ) + }) + + // = aws-encryption-sdk-specification/framework/branch-key-store.md#record-format + // = type=test + // # 1. `kms-arn`: The AWS KMS Key ARN used to generate the `enc` value. + // # represented as [AWS DDB String](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes) + it(`Active & versioned items have no ${KMS_FIELD} field`, () => { + const activeItem: BranchKeyItem = { ...ACTIVE_BRANCH_KEY } + delete activeItem[KMS_FIELD] + expect(() => validateBranchKeyRecord(activeItem)).to.throw( + `Branch keystore record does not contain ${KMS_FIELD} field of type string` + ) + + const versionedItem: BranchKeyItem = { ...VERSION_BRANCH_KEY } + delete versionedItem[KMS_FIELD] + expect(() => validateBranchKeyRecord(versionedItem)).to.throw( + `Branch keystore record does not contain ${KMS_FIELD} field of type string` + ) + }) + + // = aws-encryption-sdk-specification/framework/branch-key-store.md#record-format + // = type=test + // # 1. `create-time`: Timestamp in ISO 8601 format in UTC, to microsecond precision. + // # Represented as [AWS DDB String](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes) + it(`Active & versioned items have no ${KEY_CREATE_TIME_FIELD} field`, () => { + const activeItem: BranchKeyItem = { ...ACTIVE_BRANCH_KEY } + delete activeItem[KEY_CREATE_TIME_FIELD] + expect(() => validateBranchKeyRecord(activeItem)).to.throw( + `Branch keystore record does not contain ${KEY_CREATE_TIME_FIELD} field of type string` + ) + + const versionedItem: BranchKeyItem = { ...VERSION_BRANCH_KEY } + delete versionedItem[KEY_CREATE_TIME_FIELD] + expect(() => validateBranchKeyRecord(versionedItem)).to.throw( + `Branch keystore record does not contain ${KEY_CREATE_TIME_FIELD} field of type string` + ) + }) + + // = aws-encryption-sdk-specification/framework/branch-key-store.md#record-format + // = type=test + // # 1. `hierarchy-version`: Version of the hierarchical keyring; + // # represented as [AWS DDB Number](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes) + it(`Active & versioned items have no ${HIERARCHY_VERSION_FIELD} field`, () => { + const activeItem: BranchKeyItem = { ...ACTIVE_BRANCH_KEY } + delete activeItem[HIERARCHY_VERSION_FIELD] + expect(() => validateBranchKeyRecord(activeItem)).to.throw( + `Branch keystore record does not contain ${HIERARCHY_VERSION_FIELD} field of type number` + ) + + const versionedItem: BranchKeyItem = { ...VERSION_BRANCH_KEY } + delete versionedItem[HIERARCHY_VERSION_FIELD] + expect(() => validateBranchKeyRecord(versionedItem)).to.throw( + `Branch keystore record does not contain ${HIERARCHY_VERSION_FIELD} field of type number` + ) + }) + + // = aws-encryption-sdk-specification/framework/branch-key-store.md#record-format + // = type=test + // # A branch key record MAY include [custom encryption context](#custom-encryption-context) key-value pairs. + // # These attributes should be prefixed with `aws-crypto-ec:` the same way they are for [AWS KMS encryption context](#encryption-context). + it('Active & versioned items have additional fields prefixed properly', () => { + const activeItem = { + ...ACTIVE_BRANCH_KEY, + ...VALID_CUSTOM_ENCRYPTION_CONTEXT_KV_PAIRS, + } + expect(validateBranchKeyRecord(activeItem)).deep.equals({ + ...ACTIVE_BRANCH_KEY, + ...VALID_CUSTOM_ENCRYPTION_CONTEXT_KV_PAIRS, + }) + + const versionedItem = { + ...VERSION_BRANCH_KEY, + ...VALID_CUSTOM_ENCRYPTION_CONTEXT_KV_PAIRS, + } + expect(validateBranchKeyRecord(versionedItem)).deep.equals({ + ...VERSION_BRANCH_KEY, + ...VALID_CUSTOM_ENCRYPTION_CONTEXT_KV_PAIRS, + }) + }) + + it('Active & versioned items have additional fields prefixed improperly', () => { + const activeItem = { + ...ACTIVE_BRANCH_KEY, + ...INVALID_CUSTOM_ENCRYPTION_CONTEXT_KV_PAIRS, + } + expect(() => validateBranchKeyRecord(activeItem)).to.throw( + `Custom encryption context key ${ + Object.keys(INVALID_CUSTOM_ENCRYPTION_CONTEXT_KV_PAIRS)[0] + } should be prefixed with ${CUSTOM_ENCRYPTION_CONTEXT_FIELD_PREFIX}` + ) + + const versionedItem = { + ...VERSION_BRANCH_KEY, + ...INVALID_CUSTOM_ENCRYPTION_CONTEXT_KV_PAIRS, + } + expect(() => validateBranchKeyRecord(versionedItem)).to.throw( + `Custom encryption context key ${ + Object.keys(INVALID_CUSTOM_ENCRYPTION_CONTEXT_KV_PAIRS)[0] + } should be prefixed with ${CUSTOM_ENCRYPTION_CONTEXT_FIELD_PREFIX}` + ) + }) + }) + + // = aws-encryption-sdk-specification/framework/branch-key-store.md#encryption-context + // = type=test + // # This section describes how the AWS KMS encryption context is built + // # from the DynamoDB items that store the branch keys. + // # The following encryption context keys are shared: + // # - MUST have a `branch-key-id` attribute + // # - The `branch-key-id` field MUST not be an empty string + // # - MUST have a `type` attribute + // # - The `type` field MUST not be an empty string + // # - MUST have a `create-time` attribute + // # - MUST have a `tablename` attribute to store the logicalKeyStoreName + // # - MUST have a `kms-arn` attribute + // # - MUST have a `hierarchy-version` + // # - MUST NOT have a `enc` attribute + // # Any additionally attributes on the DynamoDB item + // # MUST be added to the encryption context. + describe('Test constructAuthenticatedEncryptionContext', () => { + it('Given active & versioned branch key records with no custom EC', () => { + const activeAuthEc = constructAuthenticatedEncryptionContext( + BRANCH_KEYSTORE, + ACTIVE_BRANCH_KEY + ) + expect(activeAuthEc).to.deep.equals( + ENCRYPTED_ACTIVE_BRANCH_KEY.encryptionContext + ) + + const versionedAuthEc = constructAuthenticatedEncryptionContext( + BRANCH_KEYSTORE, + VERSION_BRANCH_KEY + ) + expect(versionedAuthEc).to.deep.equals( + ENCRYPTED_VERSION_BRANCH_KEY.encryptionContext + ) + }) + + it('Given active & versioned branch key records with a custom EC', () => { + const activeAuthEc = constructAuthenticatedEncryptionContext( + BRANCH_KEYSTORE, + { + ...ACTIVE_BRANCH_KEY, + ...VALID_CUSTOM_ENCRYPTION_CONTEXT_KV_PAIRS, + } + ) + expect(activeAuthEc).to.deep.equals({ + ...ENCRYPTED_ACTIVE_BRANCH_KEY.encryptionContext, + ...VALID_CUSTOM_ENCRYPTION_CONTEXT, + }) + + const versionedAuthEc = constructAuthenticatedEncryptionContext( + BRANCH_KEYSTORE, + { + ...VERSION_BRANCH_KEY, + ...VALID_CUSTOM_ENCRYPTION_CONTEXT_KV_PAIRS, + } + ) + expect(versionedAuthEc).to.deep.equals({ + ...ENCRYPTED_VERSION_BRANCH_KEY.encryptionContext, + ...VALID_CUSTOM_ENCRYPTION_CONTEXT, + }) + }) + }) + + describe('Test decryptBranchKey', () => { + //= aws-encryption-sdk-specification/framework/branch-key-store.md#aws-kms-branch-key-decryption + //= type=test + //# If the Keystore's [AWS KMS Configuration](#aws-kms-configuration) is `KMS Key ARN` or `KMS MRKey ARN`, + //# the `kms-arn` field of the DDB response item MUST be + //# [compatible with](#aws-key-arn-compatibility) the configured KMS Key in + //# the [AWS KMS Configuration](#aws-kms-configuration) for this keystore, + //# or the operation MUST fail. + //# If the Keystore's [AWS KMS Configuration](#aws-kms-configuration) is `Discovery` or `MRDiscovery`, + //# the `kms-arn` field of DDB response item MUST NOT be an Alias + //# or the operation MUST fail. + it("Active & versioned DDB records' kms-arn's are compatible with KMS config's", async () => { + const configArn = KEY_ARN + + // create a real up-to-date active branch key record + const activeEncryptedBranchKey = + await BRANCH_KEY_STORAGE.getEncryptedActiveBranchKey(BRANCH_KEY_ID) + + const activeBranchKey = await decryptBranchKey( + BRANCH_KEYSTORE, + activeEncryptedBranchKey + ) + + const versionedBranchKey = await decryptBranchKey( + BRANCH_KEYSTORE, + activeEncryptedBranchKey + ) + + const kmsClient = new KMSClient({ + region: getRegionFromIdentifier(configArn), + }) + + let response = await kmsClient.send( + new DecryptCommand({ + KeyId: configArn, + CiphertextBlob: activeEncryptedBranchKey.ciphertextBlob, + EncryptionContext: activeEncryptedBranchKey.encryptionContext, + }) + ) + const expectedActiveBranchKey = Buffer.from( + response.Plaintext as Uint8Array + ) + + // = aws-encryption-sdk-specification/framework/branch-key-store.md#aws-kms-branch-key-decryption + // = type=test + // # When calling [AWS KMS Decrypt](https://docs.aws.amazon.com/kms/latest/APIReference/API_Decrypt.html), + // # the keystore operation MUST call with a request constructed as follows: + // # - `KeyId`, if the KMS Configuration is Discovery, MUST be the `kms-arn` attribute value of the AWS DDB response item. + // # If the KMS Configuration is MRDiscovery, `KeyId` MUST be the `kms-arn` attribute value of the AWS DDB response item, with the region replaced by the configured region. + // # Otherwise, it MUST BE the Keystore's configured KMS Key. + // # - `CiphertextBlob` MUST be the `enc` attribute value on the AWS DDB response item + // # - `EncryptionContext` MUST be the [encryption context](#encryption-context) constructed above + // # - `GrantTokens` MUST be this keystore's [grant tokens](https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#grant_token). + response = await kmsClient.send( + new DecryptCommand({ + KeyId: configArn, + CiphertextBlob: VERSION_BRANCH_KEY[BRANCH_KEY_FIELD], + EncryptionContext: ENCRYPTED_VERSION_BRANCH_KEY.encryptionContext, + }) + ) + const expectedVersionedBranchKey = Buffer.from( + response.Plaintext as Uint8Array + ) + + expect(activeBranchKey).to.deep.equals(expectedActiveBranchKey) + expect(versionedBranchKey).to.deep.equals(expectedVersionedBranchKey) + }) + + it("Active & versioned DDB records' kms-arn's are incompatible with KMS config's", async () => { + const configArn = KEY_ARN.replace('0', '1') + const branchKeyStore = new BranchKeyStoreNode({ + storage: { ddbTableName: DDB_TABLE_NAME }, + logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, + kmsConfiguration: { identifier: configArn }, + }) + + // create a real up-to-date active branch key record + const activeBranchKeyRecord = + await branchKeyStore.storage.getEncryptedActiveBranchKey(BRANCH_KEY_ID) + + void (await expect( + decryptBranchKey(branchKeyStore, activeBranchKeyRecord) + ).to.be.rejectedWith( + 'KMS ARN from DDB response item MUST be compatible with the configured KMS Key in the AWS KMS Configuration for this keystore' + )) + + void (await expect( + decryptBranchKey(branchKeyStore, ENCRYPTED_VERSION_BRANCH_KEY) + ).to.be.rejectedWith( + 'KMS ARN from DDB response item MUST be compatible with the configured KMS Key in the AWS KMS Configuration for this keystore' + )) + }) + + it('Active & versioned DDB records have custom EC', async () => { + const configArn = + 'arn:aws:kms:us-west-2:370957321024:key/9d989aa2-2f9c-438c-a745-cc57d3ad0126' + const branchKeyId = '5ad89fbc-8011-4e18-95d5-31b165d8a10e' + const branchKeyStore = new BranchKeyStoreNode({ + storage: { ddbTableName: DDB_TABLE_NAME }, + logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, + kmsConfiguration: { identifier: configArn }, + }) + + const activeBranchKeyRecord = + await branchKeyStore.storage.getEncryptedActiveBranchKey(branchKeyId) + + const activeBranchKey = await decryptBranchKey( + branchKeyStore, + activeBranchKeyRecord + ) + + const version = '8b867b79-3890-4f9b-9068-161fbc81ab3d' + const versionedBranchKeyRecord = + await branchKeyStore.storage.getEncryptedBranchKeyVersion( + branchKeyId, + version + ) + const versionedBranchKey = await decryptBranchKey( + branchKeyStore, + versionedBranchKeyRecord + ) + + const kmsClient = new KMSClient({ + region: getRegionFromIdentifier(configArn), + }) + let response = await kmsClient.send( + new DecryptCommand({ + KeyId: configArn, + CiphertextBlob: activeBranchKeyRecord.ciphertextBlob, + EncryptionContext: activeBranchKeyRecord.encryptionContext, + }) + ) + const expectedActiveBranchKey = Buffer.from( + response.Plaintext as Uint8Array + ) + + response = await kmsClient.send( + new DecryptCommand({ + KeyId: configArn, + CiphertextBlob: versionedBranchKeyRecord.ciphertextBlob, + EncryptionContext: versionedBranchKeyRecord.encryptionContext, + }) + ) + const expectedVersionedBranchKey = Buffer.from( + response.Plaintext as Uint8Array + ) + + expect(activeBranchKey).deep.equals(expectedActiveBranchKey) + expect(versionedBranchKey).deep.equals(expectedVersionedBranchKey) + }) + }) + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#branch-key-materials-from-authenticated-encryption-context + //= type=test + //# To construct [branch key materials](./structures.md#branch-key-materials) from authenticated encryption context as follows: + //# - [Branch Key](./structures.md#branch-key) MUST be the [decrypted branch key material](#aws-kms-branch-key-decryption) + //# - [Branch Key Id](./structures.md#branch-key-id) MUST be the `branch-key-id` + //# - [Branch Key Version](./structures.md#branch-key-version) + //# The version string MUST start with `branch:version:`. + //# The remaining string encoded as UTF8 bytes MUST be the Branch Key version. + //# - [Encryption Context](./structures.md#encryption-context-3) MUST be constructed by + //# [Custom Encryption Context From Authenticated Encryption Context](#custom-encryption-context-from-authenticated-encryption-context) + describe('Test constructBranchKeyMaterials', () => { + const branchKey = Buffer.alloc(32) + + it('Given active & versioned branch authenticated ECs with no custom EC', () => { + const activeAuthEc = ENCRYPTED_ACTIVE_BRANCH_KEY.encryptionContext + const activeBranchKeyMaterials = constructBranchKeyMaterials( + branchKey, + ENCRYPTED_ACTIVE_BRANCH_KEY + ) + const versionedAuthEc = ENCRYPTED_VERSION_BRANCH_KEY.encryptionContext + const versionedBranchKeyMaterials = constructBranchKeyMaterials( + branchKey, + ENCRYPTED_VERSION_BRANCH_KEY + ) + + expect(activeBranchKeyMaterials.branchKey()).deep.equals(branchKey) + expect(versionedBranchKeyMaterials.branchKey()).deep.equals(branchKey) + + expect(activeBranchKeyMaterials.branchKeyIdentifier).equals(BRANCH_KEY_ID) + expect(versionedBranchKeyMaterials.branchKeyIdentifier).equals( + BRANCH_KEY_ID + ) + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#branch-key-materials-from-authenticated-encryption-context + //= type=test + //# If the `type` attribute is equal to `"branch:ACTIVE"` + //# then the authenticated encryption context MUST have a `version` attribute + //# and the version string is this value. + //# If the `type` attribute start with `"branch:version:"` then the version string MUST be equal to this value. + expect(activeBranchKeyMaterials.branchKeyVersion).deep.equals( + Buffer.from( + activeAuthEc[BRANCH_KEY_ACTIVE_VERSION_FIELD].substring( + BRANCH_KEY_TYPE_PREFIX.length + ), + 'utf-8' + ) + ) + expect(versionedBranchKeyMaterials.branchKeyVersion).deep.equals( + Buffer.from( + versionedAuthEc[TYPE_FIELD].substring(BRANCH_KEY_TYPE_PREFIX.length), + 'utf-8' + ) + ) + + expect(activeBranchKeyMaterials.encryptionContext).deep.equals({}) + expect(versionedBranchKeyMaterials.encryptionContext).deep.equals({}) + }) + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#custom-encryption-context-from-authenticated-encryption-context + //= type=test + //# For every key in the [encryption context](./structures.md#encryption-context-3) + //# the string `aws-crypto-ec:` + the UTF8 decode of this key + //# MUST exist as a key in the authenticated encryption context. + //# Also, the value in the [encryption context](./structures.md#encryption-context-3) for this key + //# MUST equal the value in the authenticated encryption context + //# for the constructed key. + it('Given active & versioned branch authenticated ECs with a custom EC', () => { + const activeBranchKeyMaterials = constructBranchKeyMaterials( + branchKey, + new EncryptedHierarchicalKey( + { + ...ENCRYPTED_ACTIVE_BRANCH_KEY.encryptionContext, + ...VALID_CUSTOM_ENCRYPTION_CONTEXT, + }, + ENCRYPTED_ACTIVE_BRANCH_KEY.ciphertextBlob + ) + ) + expect(activeBranchKeyMaterials.branchKey()).deep.equals(branchKey) + expect(activeBranchKeyMaterials.branchKeyIdentifier).equals(BRANCH_KEY_ID) + expect(activeBranchKeyMaterials.branchKeyVersion).deep.equals( + Buffer.from(ENCRYPTED_ACTIVE_BRANCH_KEY.type.version, 'utf-8') + ) + expect(activeBranchKeyMaterials.encryptionContext).deep.equals( + VALID_CUSTOM_ENCRYPTION_CONTEXT + ) + + const versionedBranchKeyMaterials = constructBranchKeyMaterials( + branchKey, + new EncryptedHierarchicalKey( + { + ...ENCRYPTED_VERSION_BRANCH_KEY.encryptionContext, + ...VALID_CUSTOM_ENCRYPTION_CONTEXT, + }, + ENCRYPTED_VERSION_BRANCH_KEY.ciphertextBlob + ) + ) + expect(versionedBranchKeyMaterials.branchKey()).deep.equals(branchKey) + expect(versionedBranchKeyMaterials.branchKeyIdentifier).equals( + BRANCH_KEY_ID + ) + expect(versionedBranchKeyMaterials.branchKeyVersion).deep.equals( + Buffer.from(ENCRYPTED_VERSION_BRANCH_KEY.type.version, 'utf-8') + ) + expect(versionedBranchKeyMaterials.encryptionContext).deep.equals( + VALID_CUSTOM_ENCRYPTION_CONTEXT + ) + }) + }) +}) diff --git a/modules/branch-keystore-node/test/fixtures.ts b/modules/branch-keystore-node/test/fixtures.ts new file mode 100644 index 000000000..6dcaa164c --- /dev/null +++ b/modules/branch-keystore-node/test/fixtures.ts @@ -0,0 +1,101 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { BranchKeyRecord } from '../src/branch_keystore_structures' +import { EncryptedHierarchicalKey, BranchKeyVersionType } from '../src/types' +import { + BRANCH_KEY_ACTIVE_TYPE, + BRANCH_KEY_ACTIVE_VERSION_FIELD, + BRANCH_KEY_FIELD, + BRANCH_KEY_IDENTIFIER_FIELD, + HIERARCHY_VERSION_FIELD, + KEY_CREATE_TIME_FIELD, + KMS_FIELD, + TYPE_FIELD, + TABLE_FIELD, +} from '../src/constants' + +export const DDB_TABLE_NAME = 'KeyStoreDdbTable' +export const LOGICAL_KEYSTORE_NAME = DDB_TABLE_NAME +export const BRANCH_KEY_ID = '75789115-1deb-4fe3-a2ec-be9e885d1945' +export const BRANCH_KEY_ACTIVE_VERSION = 'fed7ad33-0774-4f97-aa5e-6c766fc8af9f' +export const BRANCH_KEY_ID_WITH_EC = '4bb57643-07c1-419e-92ad-0df0df149d7c' +export const BRANCH_KEY_ACTIVE_VERSION_UTF8_BYTES = Buffer.from( + BRANCH_KEY_ACTIVE_VERSION, + 'utf-8' +) +export const KEY_ARN = + 'arn:aws:kms:us-west-2:370957321024:key/9d989aa2-2f9c-438c-a745-cc57d3ad0126' +export const KEY_ID = '9d989aa2-2f9c-438c-a745-cc57d3ad0126' +export const POSTAL_HORN_BRANCH_KEY_ID = '682dfba7-4c35-491d-8d6a-5a9c56194061' +export const KMS_KEY_ALIAS = + 'arn:aws:kms:us-west-2:370957321024:alias/postalHorn' +export const INCORRECT_LOGICAL_NAME = 'MySuperAwesomeTableName' +export const POSTAL_HORN_KEY_ARN = + 'arn:aws:kms:us-west-2:370957321024:key/bc127593-f7da-452c-a1f3-cd34c46f81f8' +export const LYING_BRANCH_KEY_ID = 'kms-arn-attribute-is-lying' +export const LYING_BRANCH_KEY_DECRYPT_ONLY_VERSION = + '129c5c87-308a-41c9-8b9d-a27f66e915f4' + +// may not be active currently, but serves structural purpose +const ENCRYPTED_ACTIVE_BRANCH_KEY_CIPHERTEXT_BASE64 = + 'AQICAHhTIzkciiF5TDB8qaCjctFmv6Dx+AQICAHhTIzkciiF5TDB8qaCjctFmv6Dx+4yjarauOA4MtH0jwgFHXGFS6janEEbpRnd0qbBJAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMQLI9FLotey+qbs/CAgEQgDtqHnL1epEEpixeJCOG16V4cozeww9wMc82h7SSvXHP9PHTycAScLYZi2YICMka+QnZmPj4qP/9mb1xWQ==/7VWpSPAgEQgDuxKdGTboqxDhxBV1FQUVia8OFaQsLlPkuhwgc82tMhH9T2vAvsHGZPyPoK8zCG2xEjo3KIos8N1YK7mA==' +const ENCRYPTED_ACTIVE_BRANCH_KEY_CIPHERTEXT = new Uint8Array( + // @ts-ignore + Buffer.from(ENCRYPTED_ACTIVE_BRANCH_KEY_CIPHERTEXT_BASE64, 'base64') +) + +export const ENCRYPTED_ACTIVE_BRANCH_KEY = new EncryptedHierarchicalKey( + { + [BRANCH_KEY_IDENTIFIER_FIELD]: BRANCH_KEY_ID, + [TYPE_FIELD]: BRANCH_KEY_ACTIVE_TYPE, + [BRANCH_KEY_ACTIVE_VERSION_FIELD]: + `branch:version:${BRANCH_KEY_ACTIVE_VERSION}` as BranchKeyVersionType, + [KEY_CREATE_TIME_FIELD]: '2023-07-12T17:34:06:000290Z', + [HIERARCHY_VERSION_FIELD]: '1', + [KMS_FIELD]: KEY_ARN, + [TABLE_FIELD]: LOGICAL_KEYSTORE_NAME, + }, + ENCRYPTED_ACTIVE_BRANCH_KEY_CIPHERTEXT +) + +const ENCRYPTED_VERSION_BRANCH_KEY_CIPHERTEXT_BASE64 = + 'AQIBAHhTIzkciiF5TDB8qaCjctFmv6Dx+4yjarauOA4MtH0jwgFcb8VH4blkX0w7e59l8tl4AAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQM2tJUaqT5i07TTV9FAgEQgDsWBTM/N+rN+N7A1Js6TXVxbb64vt8eQ+G2LUs5yy98l11pXe78HZKnD+/YoUevUY1YDskV3ATRE+x2+g==' +const ENCRYPTED_VERSION_BRANCH_KEY_CIPHERTEXT = new Uint8Array( + // @ts-ignore + Buffer.from(ENCRYPTED_VERSION_BRANCH_KEY_CIPHERTEXT_BASE64, 'base64') +) + +export const ENCRYPTED_VERSION_BRANCH_KEY = new EncryptedHierarchicalKey( + { + [BRANCH_KEY_IDENTIFIER_FIELD]: BRANCH_KEY_ID, + [TYPE_FIELD]: + `branch:version:${BRANCH_KEY_ACTIVE_VERSION}` as BranchKeyVersionType, + [KEY_CREATE_TIME_FIELD]: '2023-07-12T17:34:06:000290Z', + [HIERARCHY_VERSION_FIELD]: '1', + [KMS_FIELD]: KEY_ARN, + [TABLE_FIELD]: LOGICAL_KEYSTORE_NAME, + }, + ENCRYPTED_VERSION_BRANCH_KEY_CIPHERTEXT +) + +export const ACTIVE_BRANCH_KEY: BranchKeyRecord = { + [BRANCH_KEY_IDENTIFIER_FIELD]: BRANCH_KEY_ID, + [TYPE_FIELD]: BRANCH_KEY_ACTIVE_TYPE, + [BRANCH_KEY_ACTIVE_VERSION_FIELD]: + `branch:version:${BRANCH_KEY_ACTIVE_VERSION}` as BranchKeyVersionType, + [KEY_CREATE_TIME_FIELD]: '2023-07-12T17:34:06:000290Z', + [HIERARCHY_VERSION_FIELD]: 1, + [KMS_FIELD]: KEY_ARN, + [BRANCH_KEY_FIELD]: ENCRYPTED_ACTIVE_BRANCH_KEY_CIPHERTEXT, +} + +export const VERSION_BRANCH_KEY: BranchKeyRecord = { + [BRANCH_KEY_IDENTIFIER_FIELD]: BRANCH_KEY_ID, + [TYPE_FIELD]: + `branch:version:${BRANCH_KEY_ACTIVE_VERSION}` as BranchKeyVersionType, + [KEY_CREATE_TIME_FIELD]: '2023-07-12T17:34:06:000290Z', + [HIERARCHY_VERSION_FIELD]: 1, + [KMS_FIELD]: KEY_ARN, + [BRANCH_KEY_FIELD]: ENCRYPTED_VERSION_BRANCH_KEY_CIPHERTEXT, +} diff --git a/modules/branch-keystore-node/test/kms_config.test.ts b/modules/branch-keystore-node/test/kms_config.test.ts new file mode 100644 index 000000000..2f33f3be1 --- /dev/null +++ b/modules/branch-keystore-node/test/kms_config.test.ts @@ -0,0 +1,239 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { expect } from 'chai' +import { + KmsKeyConfig, + RegionalKmsConfig, + KmsConfig, +} from '../src/kms_config' + +function supplySrkKmsConfig(config: KmsConfig): KmsKeyConfig { + return new KmsKeyConfig(config) +} + +// causes parseAwsKmsKeyArn to return false +export const ONE_PART_ARN = 'mrk-12345678123412341234123456789012' +// causes parseAwsKmsKeyArn to throw an error +export const MALFORMED_ARN = + 'arn:aws:kms:us-west-2:key/9d989aa2-2f9c-438c-a745-cc57d3ad0126' +export const WELL_FORMED_SRK_ARN = + 'arn:aws:kms:us-west-2:370957321024:key/9d989aa2-2f9c-438c-a745-cc57d3ad0126' +export const WELL_FORMED_MRK_ARN = + 'arn:aws:kms:us-west-2:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7' +export const WELL_FORMED_MRK_ARN_DIFFERENT_REGION = + 'arn:aws:kms:us-east-1:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7' +export const WELL_FORMED_SRK_ALIAS_ARN = + 'arn:aws:kms:us-west-2:123456789012:alias/srk/my-srk-alias' +export const WELL_FORMED_MRK_ALIAS_ARN = + 'arn:aws:kms:us-west-2:123456789012:alias/mrk/my-mrk-alias' + +describe('Test KmsKeyConfig class', () => { + it('Precondition: ARN must be a string', () => { + for (const arn of [null, undefined, 0, {}]) { + expect(() => supplySrkKmsConfig(arn as any)).to.throw( + 'ARN must be a string' + ) + } + }) + + describe('Given a well formed SRK arn', () => { + //= aws-encryption-sdk-specification/framework/branch-key-store.md#aws-kms-configuration + //= type=test + //# `KMS Key ARN` and `KMS MRKey ARN` MUST take an additional argument + //# that is a KMS ARN. + const config = supplySrkKmsConfig({ identifier: WELL_FORMED_SRK_ARN }) + + it('Test getRegion', () => { + expect(config.getRegion()).equals('us-west-2') + }) + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#aws-key-arn-compatibility + //= type=test + //# For two ARNs to be compatible: + //# + //# If the [AWS KMS Configuration](#aws-kms-configuration) designates single region ARN compatibility, + //# then two ARNs are compatible if they are exactly equal. + describe('Test isCompatibleWithArn', () => { + it('Given an equal arn', () => { + expect(config.isCompatibleWithArn(WELL_FORMED_SRK_ARN)).equals(true) + }) + + it('Given a non-equal arn', () => { + expect(config.isCompatibleWithArn(WELL_FORMED_SRK_ALIAS_ARN)).equals( + false + ) + }) + }) + + describe('Test getCompatibleArnArn', () => { + + it('Returns the SRK', () => { + expect(config.getCompatibleArnArn(WELL_FORMED_SRK_ARN)).to.equal(WELL_FORMED_SRK_ARN) + }) + + it('Throws for a non compatible value', () => { + expect(() => config.getCompatibleArnArn(WELL_FORMED_MRK_ARN)).to.throw() + }) + + }) + }) + + describe('Given a well formed MRK arn', () => { + const config = supplySrkKmsConfig({ identifier: WELL_FORMED_MRK_ARN }) + + it('Test getRegion', () => { + expect((config as RegionalKmsConfig).getRegion()).equals('us-west-2') + }) + + describe('Test isCompatibleWithArn', () => { + it('Given an equal arn', () => { + expect(config.isCompatibleWithArn(WELL_FORMED_MRK_ARN)).equals(true) + }) + + it('Given a non-equal arn', () => { + expect(config.isCompatibleWithArn(WELL_FORMED_MRK_ALIAS_ARN)).equals( + false + ) + }) + + it('Given an equal mkr arn', () => { + expect( + supplySrkKmsConfig({ + mrkIdentifier: WELL_FORMED_MRK_ARN, + }).isCompatibleWithArn(WELL_FORMED_MRK_ARN) + ).equals(true) + }) + + it('Given an equal mkr arn in a different region', () => { + expect( + supplySrkKmsConfig({ + mrkIdentifier: WELL_FORMED_MRK_ARN, + }).isCompatibleWithArn(WELL_FORMED_MRK_ARN_DIFFERENT_REGION) + ).equals(true) + }) + }) + + describe('Test getCompatibleArnArn', () => { + + it('Returns the MRK', () => { + expect(config.getCompatibleArnArn(WELL_FORMED_MRK_ARN)).to.equal(WELL_FORMED_MRK_ARN) + }) + + it('Returns the configured MRK because it is the right region', () => { + expect(config.getCompatibleArnArn(WELL_FORMED_MRK_ARN_DIFFERENT_REGION)).to.equal(WELL_FORMED_MRK_ARN) + }) + + it('Throws for a non compatible value', () => { + expect(() => config.getCompatibleArnArn(WELL_FORMED_SRK_ARN)).to.throw() + }) + + }) + }) + + describe('Given discovery configurations', () => { + + it('Discovery is compatible with ARNs', () => { + const config = supplySrkKmsConfig('discovery') + expect(config.isCompatibleWithArn(ONE_PART_ARN)).to.equal(true) + expect(config.isCompatibleWithArn(WELL_FORMED_SRK_ARN)).to.equal(true) + expect(config.isCompatibleWithArn(WELL_FORMED_MRK_ARN)).to.equal(true) + }) + + + it('MRDiscovery is compatible with ARNs', () => { + const config = supplySrkKmsConfig({region: 'us-west-2'}) + expect(config.isCompatibleWithArn(ONE_PART_ARN)).to.equal(true) + expect(config.isCompatibleWithArn(WELL_FORMED_SRK_ARN)).to.equal(true) + expect(config.isCompatibleWithArn(WELL_FORMED_MRK_ARN)).to.equal(true) + }) + + it('Discovery MUST be an ARN', () => { + const config = supplySrkKmsConfig('discovery') + expect(() => config.isCompatibleWithArn(MALFORMED_ARN)).to.throw() + expect(() => config.isCompatibleWithArn(WELL_FORMED_SRK_ALIAS_ARN)).to.throw() + expect(() => config.isCompatibleWithArn(WELL_FORMED_MRK_ALIAS_ARN)).to.throw() + }) + + + it('MRDiscovery MUST be an ARN', () => { + const config = supplySrkKmsConfig({region: 'us-west-2'}) + expect(() => config.isCompatibleWithArn(MALFORMED_ARN)).to.throw() + expect(() => config.isCompatibleWithArn(WELL_FORMED_SRK_ALIAS_ARN)).to.throw() + expect(() => config.isCompatibleWithArn(WELL_FORMED_MRK_ALIAS_ARN)).to.throw() + }) + + describe('Test getCompatibleArnArn for discovery', () => { + const config = supplySrkKmsConfig('discovery') + + it('Returns the SRK', () => { + expect(config.getCompatibleArnArn(WELL_FORMED_SRK_ARN)).to.equal(WELL_FORMED_SRK_ARN) + }) + + it('Returns the MRK', () => { + expect(config.getCompatibleArnArn(WELL_FORMED_MRK_ARN)).to.equal(WELL_FORMED_MRK_ARN) + }) + + it('Returns the configured MRK because it is the right region', () => { + expect(config.getCompatibleArnArn(WELL_FORMED_MRK_ARN_DIFFERENT_REGION)).to.equal(WELL_FORMED_MRK_ARN_DIFFERENT_REGION) + }) + + it('Throws for a non compatible value', () => { + expect(() => config.getCompatibleArnArn(WELL_FORMED_SRK_ARN)).to.throw() + }) + + }) + + describe('Test getCompatibleArnArn for MRDiscovery', () => { + const config = supplySrkKmsConfig({region: 'us-east-1'}) + + it('Returns the SRK', () => { + expect(config.getCompatibleArnArn(WELL_FORMED_SRK_ARN)).to.equal(WELL_FORMED_SRK_ARN) + }) + + it('Returns the MRK', () => { + expect(config.getCompatibleArnArn(WELL_FORMED_MRK_ARN)).to.equal(WELL_FORMED_MRK_ARN_DIFFERENT_REGION) + }) + + it('Returns the configured MRK because it is the right region', () => { + expect(config.getCompatibleArnArn(WELL_FORMED_MRK_ARN_DIFFERENT_REGION)).to.equal(WELL_FORMED_MRK_ARN_DIFFERENT_REGION) + }) + + it('Throws for a non compatible value', () => { + expect(() => config.getCompatibleArnArn(WELL_FORMED_SRK_ARN)).to.throw() + }) + + }) + + }) + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#aws-kms-configuration + //= type=test + //# This ARN MUST NOT be an Alias. + //# This ARN MUST be a valid + //# [AWS KMS Key ARN](./aws-kms/aws-kms-key-arn.md#a-valid-aws-kms-arn). + it('Given arns that are not parseable AWS KMS arns', () => { + expect(() => supplySrkKmsConfig({ identifier: MALFORMED_ARN })).to.throw( + 'Malformed arn.' + ) + expect(() => supplySrkKmsConfig({ identifier: ONE_PART_ARN })).to.throw( + `${ONE_PART_ARN} must be a well-formed AWS KMS non-alias resource arn` + ) + }) + + it('Given a well formed SRK alias arn', () => { + expect(() => + supplySrkKmsConfig({ identifier: WELL_FORMED_SRK_ALIAS_ARN }) + ).to.throw( + `${WELL_FORMED_SRK_ALIAS_ARN} must be a well-formed AWS KMS non-alias resource arn` + ) + }) + + it('Given a well formed MRK alias arn', () => { + expect(() => + supplySrkKmsConfig({ identifier: WELL_FORMED_MRK_ALIAS_ARN }) + ).to.throw( + `${WELL_FORMED_MRK_ALIAS_ARN} must be a well-formed AWS KMS non-alias resource arn` + ) + }) +}) diff --git a/modules/branch-keystore-node/tsconfig.json b/modules/branch-keystore-node/tsconfig.json new file mode 100644 index 000000000..c64db1039 --- /dev/null +++ b/modules/branch-keystore-node/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../tsconfig.settings.json", + "compilerOptions": { + "outDir": "build/main", + "rootDir": "./" + }, + "include": ["src/**/*.ts", "test/**/*.ts"], + "exclude": ["node_modules/**"], + "references": [ + { "path": "../material-management" }, + { "path": "../kms-keyring" } + ] +} diff --git a/modules/branch-keystore-node/tsconfig.module.json b/modules/branch-keystore-node/tsconfig.module.json new file mode 100644 index 000000000..50bf04db4 --- /dev/null +++ b/modules/branch-keystore-node/tsconfig.module.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig", + "compilerOptions": { + "target": "esnext", + "outDir": "build/module", + "module": "esnext", + "allowSyntheticDefaultImports": true + }, + "exclude": [ + "node_modules/**" + ] +} \ No newline at end of file diff --git a/modules/cache-material/package.json b/modules/cache-material/package.json index f141893e5..ec704e0ed 100644 --- a/modules/cache-material/package.json +++ b/modules/cache-material/package.json @@ -22,7 +22,8 @@ "@aws-crypto/serialize": "file:../serialize", "@types/lru-cache": "^5.1.0", "lru-cache": "^6.0.0", - "tslib": "^2.2.0" + "tslib": "^2.2.0", + "util": "^0.12.5" }, "sideEffects": false, "main": "./build/main/src/index.js", diff --git a/modules/cache-material/src/cryptographic_materials_cache.ts b/modules/cache-material/src/cryptographic_materials_cache.ts index 068aa4d18..08953a072 100644 --- a/modules/cache-material/src/cryptographic_materials_cache.ts +++ b/modules/cache-material/src/cryptographic_materials_cache.ts @@ -5,6 +5,7 @@ import { EncryptionMaterial, DecryptionMaterial, SupportedAlgorithmSuites, + BranchKeyMaterial, } from '@aws-crypto/material-management' export interface CryptographicMaterialsCache< @@ -16,16 +17,30 @@ export interface CryptographicMaterialsCache< plaintextLength: number, maxAge?: number ): void + putDecryptionMaterial( key: string, response: DecryptionMaterial, maxAge?: number ): void + + // a put operation to support branch key material + putBranchKeyMaterial( + key: string, + response: BranchKeyMaterial, + maxAge?: number + ): void + getEncryptionMaterial( key: string, plaintextLength: number ): EncryptionMaterialEntry | false + getDecryptionMaterial(key: string): DecryptionMaterialEntry | false + + // a get operation to support branch key material + getBranchKeyMaterial(key: string): BranchKeyMaterialEntry | false + del(key: string): void } @@ -45,3 +60,8 @@ export interface DecryptionMaterialEntry extends Entry { readonly response: DecryptionMaterial } + +export interface BranchKeyMaterialEntry { + readonly response: BranchKeyMaterial + readonly now: number +} diff --git a/modules/cache-material/src/get_local_cryptographic_materials_cache.ts b/modules/cache-material/src/get_local_cryptographic_materials_cache.ts index 8708b3a07..d3150bc17 100644 --- a/modules/cache-material/src/get_local_cryptographic_materials_cache.ts +++ b/modules/cache-material/src/get_local_cryptographic_materials_cache.ts @@ -9,6 +9,8 @@ import { needs, isEncryptionMaterial, isDecryptionMaterial, + BranchKeyMaterial, + isBranchKeyMaterial, } from '@aws-crypto/material-management' import { @@ -16,15 +18,22 @@ import { Entry, EncryptionMaterialEntry, DecryptionMaterialEntry, + BranchKeyMaterialEntry, } from './cryptographic_materials_cache' +// define a broader type for local CMC entries that encompass BranchKeyMaterial +// entries as well +type LocalCmcEntry = + | BranchKeyMaterialEntry + | Entry + export function getLocalCryptographicMaterialsCache< S extends SupportedAlgorithmSuites >( capacity: number, proactiveFrequency: number = 1000 * 60 ): CryptographicMaterialsCache { - const cache = new LRU>({ + const cache = new LRU>({ max: capacity, dispose(_key, value) { /* Zero out the unencrypted dataKey, when the material is removed from the cache. */ @@ -82,6 +91,7 @@ export function getLocalCryptographicMaterialsCache< cache.set(key, entry, maxAge) }, + putDecryptionMaterial( key: string, material: DecryptionMaterial, @@ -100,6 +110,23 @@ export function getLocalCryptographicMaterialsCache< cache.set(key, entry, maxAge) }, + + putBranchKeyMaterial( + key: string, + material: BranchKeyMaterial, + maxAge?: number + ): void { + /* Precondition: Only cache BranchKeyMaterial */ + needs(isBranchKeyMaterial(material), 'Malformed response.') + + const entry = Object.seal({ + response: material, + now: Date.now(), + }) + + cache.set(key, entry, maxAge) + }, + getEncryptionMaterial(key: string, plaintextLength: number) { /* Precondition: plaintextLength can not be negative. */ needs(plaintextLength >= 0, 'Malformed plaintextLength') @@ -109,11 +136,13 @@ export function getLocalCryptographicMaterialsCache< /* Postcondition: Only return EncryptionMaterial. */ needs(isEncryptionMaterial(entry.response), 'Malformed response.') - entry.bytesEncrypted += plaintextLength - entry.messagesEncrypted += 1 + const encryptionMaterialEntry = entry as EncryptionMaterialEntry + encryptionMaterialEntry.bytesEncrypted += plaintextLength + encryptionMaterialEntry.messagesEncrypted += 1 return entry as EncryptionMaterialEntry }, + getDecryptionMaterial(key: string) { const entry = cache.get(key) /* Check for early return (Postcondition): If this key does not have a DecryptionMaterial, return false. */ @@ -123,6 +152,18 @@ export function getLocalCryptographicMaterialsCache< return entry as DecryptionMaterialEntry }, + + getBranchKeyMaterial(key: string): BranchKeyMaterialEntry | false { + const entry = cache.get(key) + + /* Postcondition: If this key does not have a BranchKeyMaterial, return false */ + if (!entry) return false + + /* Postcondition: Only return BranchKeyMaterial */ + needs(isBranchKeyMaterial(entry.response), 'Malformed response.') + return entry as BranchKeyMaterialEntry + }, + del(key: string) { cache.del(key) }, diff --git a/modules/cache-material/test/get_local_cryptographic_materials_cache.test.ts b/modules/cache-material/test/get_local_cryptographic_materials_cache.test.ts index 5ef2be968..dc46e7008 100644 --- a/modules/cache-material/test/get_local_cryptographic_materials_cache.test.ts +++ b/modules/cache-material/test/get_local_cryptographic_materials_cache.test.ts @@ -10,23 +10,67 @@ import { NodeEncryptionMaterial, NodeDecryptionMaterial, AlgorithmSuiteIdentifier, + NodeBranchKeyMaterial, } from '@aws-crypto/material-management' +import { v4 } from 'uuid' const nodeSuite = new NodeAlgorithmSuite( AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16_HKDF_SHA256 ) const encryptionMaterial = new NodeEncryptionMaterial(nodeSuite, {}) const decryptionMaterial = new NodeDecryptionMaterial(nodeSuite, {}) +const branchKeyMaterial = new NodeBranchKeyMaterial( + Buffer.alloc(32), + 'id', + v4(), + {} +) describe('getLocalCryptographicMaterialsCache', () => { const { getEncryptionMaterial, getDecryptionMaterial, + getBranchKeyMaterial, del, putEncryptionMaterial, putDecryptionMaterial, + putBranchKeyMaterial, } = getLocalCryptographicMaterialsCache(100) + it('putBranchKeyMaterial', () => { + const key = 'some encryption key' + const response: any = branchKeyMaterial + + putBranchKeyMaterial(key, response) + const test = getBranchKeyMaterial(key) + if (!test) throw new Error('never') + expect(test.response === response).to.equal(true) + expect(Object.isFrozen(test.response)).to.equal(true) + }) + + it('Precondition: Only cache BranchKeyMaterial', () => { + const key = 'some decryption key' + const response: any = 'not material' + + expect(() => putBranchKeyMaterial(key, response)).to.throw() + }) + + it('Postcondition: If this key does not have a BranchKeyMaterial, return false', () => { + const test = getBranchKeyMaterial('does-not-exist') + expect(test).to.equal(false) + }) + + it('Postcondition: Only return BranchKeyMaterial', () => { + putDecryptionMaterial('key1', decryptionMaterial) + putEncryptionMaterial('key2', encryptionMaterial, 1) + + expect(() => getBranchKeyMaterial('key1')).to.throw() + expect(() => getBranchKeyMaterial('key2')).to.throw() + + putBranchKeyMaterial('key3', branchKeyMaterial) + expect(() => getBranchKeyMaterial('key3')) + }) + it('putEncryptionMaterial', () => { const key = 'some encryption key' const response: any = encryptionMaterial @@ -151,6 +195,52 @@ describe('getLocalCryptographicMaterialsCache', () => { }) describe('cache eviction', () => { + it('putBranchKeyMaterial can exceed capacity', () => { + const { getBranchKeyMaterial, putBranchKeyMaterial } = + getLocalCryptographicMaterialsCache(1) + + const key1 = 'key lost' + const key2 = 'key replace' + const response: any = branchKeyMaterial + + putBranchKeyMaterial(key1, response) + putBranchKeyMaterial(key2, response) + const lost = getBranchKeyMaterial(key1) + const found = getBranchKeyMaterial(key2) + expect(lost).to.equal(false) + expect(found).to.not.equal(false) + }) + + it('putBranchKeyMaterial can be deleted', () => { + const { getBranchKeyMaterial, putBranchKeyMaterial, del } = + getLocalCryptographicMaterialsCache(1) + + const key = 'key deleted' + const response: any = branchKeyMaterial + + putBranchKeyMaterial(key, response) + del(key) + const lost = getBranchKeyMaterial(key) + expect(lost).to.equal(false) + }) + + it('putBranchKeyMaterial can be garbage collected', async () => { + const { getBranchKeyMaterial, putBranchKeyMaterial } = + // set TTL to 10 ms so that our branch key material entry is evicted between the + // put and get operation (which have a 20 ms gap). This will simulate a + // case where we try to query our branch key material but it was already + // garbage collected + getLocalCryptographicMaterialsCache(1, 10) + + const key = 'key lost' + const response: any = branchKeyMaterial + + putBranchKeyMaterial(key, response, 1) + await new Promise((resolve) => setTimeout(resolve, 20)) + const lost = getBranchKeyMaterial(key) + expect(lost).to.equal(false) + }) + it('putDecryptionMaterial can exceed capacity', () => { const { getDecryptionMaterial, putDecryptionMaterial } = getLocalCryptographicMaterialsCache(1) diff --git a/modules/caching-materials-manager-node/src/caching_materials_manager_node.ts b/modules/caching-materials-manager-node/src/caching_materials_manager_node.ts index e6c66c732..c7c8aab46 100644 --- a/modules/caching-materials-manager-node/src/caching_materials_manager_node.ts +++ b/modules/caching-materials-manager-node/src/caching_materials_manager_node.ts @@ -67,3 +67,7 @@ export class NodeCachingMaterialsManager _cacheEntryHasExceededLimits = cacheEntryHasExceededLimits() } +//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#appendix-a-cache-entry-identifier-formulas +//# When accessing the underlying cryptographic materials cache, +//# the hierarchical keyring MUST use the formulas specified in this appendix +//# in order to compute the [cache entry identifier](../cryptographic-materials-cache.md#cache-identifier). \ No newline at end of file diff --git a/modules/client-node/package.json b/modules/client-node/package.json index 02ad13ad7..fcc585f99 100644 --- a/modules/client-node/package.json +++ b/modules/client-node/package.json @@ -30,6 +30,8 @@ "@aws-crypto/material-management-node": "file:../material-management-node", "@aws-crypto/raw-aes-keyring-node": "file:../raw-aes-keyring-node", "@aws-crypto/raw-rsa-keyring-node": "file:../raw-rsa-keyring-node", + "@aws-crypto/branch-keystore-node": "file:../branch-keystore-node", + "@aws-crypto/kms-keyring": "file:../kms-keyring", "tslib": "^2.2.0" }, "sideEffects": false, diff --git a/modules/client-node/src/index.ts b/modules/client-node/src/index.ts index adf11cb51..5e984a497 100644 --- a/modules/client-node/src/index.ts +++ b/modules/client-node/src/index.ts @@ -8,6 +8,8 @@ export * from '@aws-crypto/caching-materials-manager-node' export * from '@aws-crypto/kms-keyring-node' export * from '@aws-crypto/raw-aes-keyring-node' export * from '@aws-crypto/raw-rsa-keyring-node' +export * from '@aws-crypto/branch-keystore-node' +export { BranchKeyIdSupplier } from '@aws-crypto/kms-keyring' import { CommitmentPolicy, diff --git a/modules/client-node/tsconfig.json b/modules/client-node/tsconfig.json index 8fe3e70f9..0d9ffce6e 100644 --- a/modules/client-node/tsconfig.json +++ b/modules/client-node/tsconfig.json @@ -14,5 +14,7 @@ { "path": "../kms-keyring-node" }, { "path": "../raw-rsa-keyring-node" }, { "path": "../raw-aes-keyring-node" }, + { "path": "../branch-keystore-node" }, + { "path": "../kms-keyring" } ] -} \ No newline at end of file +} diff --git a/modules/example-node/hkr-demo/README.md b/modules/example-node/hkr-demo/README.md new file mode 100644 index 000000000..d17d6e906 --- /dev/null +++ b/modules/example-node/hkr-demo/README.md @@ -0,0 +1,104 @@ +# H-Keyring Intern Demo Guide + +This guide provides detailed instructions on running the intern demos for the H-Keyring. + +## Prerequisites + +Before running the demos, navigate to the following directory: + +```bash +cd /private-aws-encryption-sdk-javascript-staging/modules/example-node/ +``` + +**Note:** All file paths in the CLI arguments must be absolute paths. + +## Demo 1: Performance Comparison between KMS Keyring and H-Keyring + +This demo compares the performance of the KMS Keyring and H-Keyring. In each roundtrip, a random string is generated, encrypted with a keyring, and then decrypted, expecting the original string to be returned. + +The program logs roundtrip metrics for both the KMS Keyring and H-Keyring, including runtime, call volume, and success rate. + +### Run the Demo + +To run the performance comparison demo, use the following command: + +```bash +npx ts-node hkr-demo/hkr_vs_regular.demo.ts --numRoundTrips= +``` + +**Note:** The number of roundtrips defaults to 10 if not specified. + +## Demo 2: Interoperability Test + +This demo demonstrates the interoperability between the JS H-Keyring and other H-Keyrings. + +### General Command + +To encrypt a data file or decrypt an encrypted file using the JS H-Keyring, use the following command format: + +```bash +npx ts-node hkr-demo/interop.demo.ts +``` + +### Encrypting a Data File + +To encrypt a data file with the JS H-Keyring, run: + +```bash +npx ts-node hkr-demo/interop.demo.ts encrypt +``` + +### Decrypting an Encrypted File + +To decrypt an encrypted file with the JS H-Keyring, run: + +```bash +npx ts-node hkr-demo/interop.demo.ts decrypt +``` + +## Demo 3: Multi-Tenancy + +This demo showcases multi-tenant data isolation within a single keyring. You will observe failures when encrypting with tenant A and decrypting with tenant B (or vice versa). Tenant A and B are mapped to hard-coded branch IDs within the demo code in `./hkr-demo/multi_tenant.demo.ts`. + +### General Command + +To encrypt or decrypt with tenant A or B, use the following command format: + +```bash +npx ts-node hkr-demo/multi_tenant.demo.ts --operation= --inputFile= --outputFile= --tenant= +``` + +### Encrypting with Tenant A + +```bash +npx ts-node hkr-demo/multi_tenant.demo.ts --operation=encrypt --inputFile= --outputFile= --tenant=A +``` + +### Decrypting with Tenant A + +```bash +npx ts-node hkr-demo/multi_tenant.demo.ts --operation=decrypt --inputFile= --outputFile= --tenant=A +``` + +### Encrypting with Tenant B + +```bash +npx ts-node hkr-demo/multi_tenant.demo.ts --operation=encrypt --inputFile= --outputFile= --tenant=B +``` + +### Decrypting with Tenant B + +```bash +npx ts-node hkr-demo/multi_tenant.demo.ts --operation=decrypt --inputFile= --outputFile= --tenant=B +``` + +### Example: Demonstrating Tenant Data Isolation + +To observe tenant data isolation, run the following commands: + +```bash +npx ts-node hkr-demo/multi_tenant.demo.ts --operation=encrypt --inputFile= --outputFile= --tenant=A +npx ts-node hkr-demo/multi_tenant.demo.ts --operation=decrypt --inputFile= --outputFile= --tenant=B +``` + +An error will occur, demonstrating the isolation between tenant encryption. diff --git a/modules/example-node/hkr-demo/hkr.ts b/modules/example-node/hkr-demo/hkr.ts new file mode 100644 index 000000000..22c752336 --- /dev/null +++ b/modules/example-node/hkr-demo/hkr.ts @@ -0,0 +1,132 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + buildClient, + CommitmentPolicy, + KeyringNode, + EncryptionContext, +} from '@aws-crypto/client-node' +import { randomBytes } from 'crypto' +import { KMSClient } from '@aws-sdk/client-kms' +import sinon from 'sinon' +import { DynamoDBClient } from '@aws-sdk/client-dynamodb' + +const { encrypt, decrypt } = buildClient( + CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT +) +const MAX_INPUT_LENGTH = 20 +const MIN_INPUT_LENGTH = 15 +const PURPLE_LOG = '\x1b[35m%s\x1b[0m' +const YELLO_LOG = '\x1b[33m%s\x1b[0m' +const GREEN_LOG = '\x1b[32m%s\x1b[0m' +const RED_LOG = '\x1b[31m%s\x1b[0m' + +// function to generate a random string +export function generateRandomString(minLength: number, maxLength: number) { + const randomLength = + Math.floor(Math.random() * (maxLength - minLength + 1)) + minLength + return randomBytes(randomLength).toString('hex').slice(0, randomLength) +} + +// function to encrypt, decrypt, and verify +export async function roundtrip( + keyring: KeyringNode, + context: EncryptionContext, + cleartext: string +) { + const { result } = await encrypt(keyring, cleartext, { + encryptionContext: context, + }) + + const { plaintext, messageHeader } = await decrypt(keyring, result) + + const { encryptionContext } = messageHeader + + Object.entries(context).forEach(([key, value]) => { + if (encryptionContext[key] !== value) { + throw new Error('Encryption Context does not match expected values') + } + }) + + return { plaintext, result, cleartext, messageHeader } +} + +// run the roundtrips on the specified keyring +export async function runRoundTrips( + keyring: KeyringNode, + numRoundTrips: number +) { + // set up spies to monitor network call volume + const kmsSpy = sinon.spy(KMSClient.prototype, 'send') + const ddbSpy = sinon.spy(DynamoDBClient.prototype, 'send') + const padding = String(numRoundTrips).length + let successes = 0 + + console.log() + console.log(YELLO_LOG, `${keyring.constructor.name} Roundtrips`) // Print constructor name in yellow + console.time('Total runtime') // Start the timer + + // for each roundtrip + for (let i = 0; i < numRoundTrips; i++) { + // create an encryption context + const encryptionContext = { + roundtrip: i.toString(), + } + // generate a random string + const encryptionInput = generateRandomString( + MIN_INPUT_LENGTH, + MAX_INPUT_LENGTH + ) + + // try to do the roundtrip. If any error arises, log it properly + let decryptionOutput: string + try { + const { plaintext } = await roundtrip( + keyring, + encryptionContext, + encryptionInput + ) + decryptionOutput = plaintext.toString() + } catch { + decryptionOutput = 'ERROR' + } + + const encryptionInputPadding = ' '.repeat( + MAX_INPUT_LENGTH - encryptionInput.length + ) + const decryptionOutputPadding = ' '.repeat( + MAX_INPUT_LENGTH - decryptionOutput.length + ) + + // log message + const logMessage = `Roundtrip ${String(i + 1).padStart( + padding, + ' ' + )}: ${encryptionInput}${encryptionInputPadding} ----encrypt & decrypt----> ${decryptionOutput}${decryptionOutputPadding}` + + // print the log green if successful. Otherwise, red + let logColor: string + if (encryptionInput === decryptionOutput) { + logColor = GREEN_LOG + successes += 1 + } else { + logColor = RED_LOG + } + console.log(logColor, logMessage) + } + + // print metrics for runtime and call volume + console.log() + console.log(YELLO_LOG, `${keyring.constructor.name} metrics`) // Print constructor name in yellow + console.timeEnd('Total runtime') + console.log(PURPLE_LOG, `KMS calls: ${kmsSpy.callCount}`) + console.log(PURPLE_LOG, `DynamoDB calls: ${ddbSpy.callCount}`) + console.log( + PURPLE_LOG, + `Successful roundtrips: ${successes} / ${numRoundTrips}` + ) + + kmsSpy.restore() + ddbSpy.restore() +} diff --git a/modules/example-node/hkr-demo/hkr_vs_regular.demo.ts b/modules/example-node/hkr-demo/hkr_vs_regular.demo.ts new file mode 100644 index 000000000..21bd0cddf --- /dev/null +++ b/modules/example-node/hkr-demo/hkr_vs_regular.demo.ts @@ -0,0 +1,52 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + BranchKeyStoreNode, + SrkCompatibilityKmsConfig, + KmsHierarchicalKeyRingNode, + KmsKeyringNode, +} from '@aws-crypto/client-node' +import { runRoundTrips } from './hkr' +import minimist from 'minimist' + +// get cli args +const args = minimist(process.argv.slice(2)) +const NUM_ROUNDTRIPS = args.numRoundTrips || 10 + +// function to run the KMS keyring roundtrips +async function runKmsKeyring() { + const generatorKeyId = + 'arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f' + const keyring = new KmsKeyringNode({ generatorKeyId }) + + await runRoundTrips(keyring, NUM_ROUNDTRIPS) +} + +// function to run the H-keyring roundtrips +async function runKmsHkrKeyring() { + const branchKeyArn = + 'arn:aws:kms:us-west-2:370957321024:key/9d989aa2-2f9c-438c-a745-cc57d3ad0126' + const branchKeyId = '2c583585-5770-467d-8f59-b346d0ed1994' + + const keyStore = new BranchKeyStoreNode({ + ddbTableName: 'KeyStoreDdbTable', + logicalKeyStoreName: 'KeyStoreDdbTable', + kmsConfiguration: new SrkCompatibilityKmsConfig(branchKeyArn), + }) + + const keyring = new KmsHierarchicalKeyRingNode({ + branchKeyId, + keyStore, + cacheLimitTtl: 60, + }) + + await runRoundTrips(keyring, NUM_ROUNDTRIPS) +} + +async function main() { + await runKmsKeyring() + await runKmsHkrKeyring() +} + +main() diff --git a/modules/example-node/hkr-demo/interop.demo.ts b/modules/example-node/hkr-demo/interop.demo.ts new file mode 100644 index 000000000..aa1fe3b56 --- /dev/null +++ b/modules/example-node/hkr-demo/interop.demo.ts @@ -0,0 +1,103 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import * as fs from 'fs' +import { + BranchKeyStoreNode, + buildClient, + CommitmentPolicy, + KmsHierarchicalKeyRingNode, + SrkCompatibilityKmsConfig, +} from '@aws-crypto/client-node' +import { exit } from 'process' + +const { encrypt, decrypt } = buildClient( + CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT +) + +// create H-Keyring +const branchKeyArn = + 'arn:aws:kms:us-west-2:370957321024:key/9d989aa2-2f9c-438c-a745-cc57d3ad0126' +const branchKeyId = '38853b56-19c6-4345-9cb5-afc2a25dcdd1' + +const keyStore = new BranchKeyStoreNode({ + ddbTableName: 'KeyStoreDdbTable', + logicalKeyStoreName: 'KeyStoreDdbTable', + kmsConfiguration: new SrkCompatibilityKmsConfig(branchKeyArn), +}) + +const keyring = new KmsHierarchicalKeyRingNode({ + branchKeyId, + keyStore, + cacheLimitTtl: 60, +}) + +// function to decrypt with H-Keyring +async function decryptEncryptedData(encryptedData: Buffer) { + const { plaintext: decryptedData, messageHeader } = await decrypt( + keyring, + encryptedData + ) + + const { encryptionContext } = messageHeader + + Object.entries(encryptionContext).forEach(([key, value]) => { + if (encryptionContext[key] !== value) { + throw new Error('Encryption Context does not match expected values') + } + }) + + return decryptedData +} + +// function to encrypt with H-Keyring +async function encryptData(data: Buffer) { + const { result } = await encrypt(keyring, data, { + encryptionContext: { successful: 'demo' }, + }) + + return result +} + +async function main() { + // read CLI args + const args = process.argv.slice(2) + const operation = args[0] + const inFile = args[1] + const outFile = args[2] + + // read from input file + let inData = Buffer.alloc(0) + try { + inData = fs.readFileSync(inFile) + } catch (err) { + console.error(err) + exit(1) + } + + // encrypt/decrypt input file + let outData: Buffer + let msg: string + if (operation === 'encrypt') { + const data = inData + outData = await encryptData(data) + msg = 'JS has completed encryption' + } else { + const encryptedData = inData + outData = await decryptEncryptedData(encryptedData) + msg = 'JS has completed decryption' + } + + // write to output file + try { + fs.writeFileSync(outFile, outData) + } catch (err) { + console.error(err) + exit(1) + } + + // log completion message + console.log(msg) +} + +main() diff --git a/modules/example-node/hkr-demo/multi_tenant.demo.ts b/modules/example-node/hkr-demo/multi_tenant.demo.ts new file mode 100644 index 000000000..14b3b8c6b --- /dev/null +++ b/modules/example-node/hkr-demo/multi_tenant.demo.ts @@ -0,0 +1,174 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + KmsHierarchicalKeyRingNode, + BranchKeyStoreNode, + SrkCompatibilityKmsConfig, + EncryptionContext, + buildClient, + CommitmentPolicy, + KeyringNode, + BranchKeyIdSupplier, +} from '@aws-crypto/client-node' +import minimist from 'minimist' +import * as fs from 'fs' +import { exit } from 'process' + +// read CLI args +const args = minimist(process.argv.slice(2)) + +// map A and B to respective branch IDs +const tenantMap: { [key: string]: string } = { + A: '38853b56-19c6-4345-9cb5-afc2a25dcdd1', + B: '2c583585-5770-467d-8f59-b346d0ed1994', +} + +// preprocess CLI args and return them under an object with named fields +function getCliArgs() { + const operation = args.operation + if (!operation) { + throw new Error('Must specify operation to perform') + } + + let inFile: string = args.inputFile + if (!inFile) { + throw new Error("Must specify input's file path") + } + inFile = inFile.replace('~', '/Users/nvobilis') + + let outFile = args.outputFile + if (!outFile) { + throw new Error("Must specify output's file path") + } + outFile = outFile.replace('~', '/Users/nvobilis') + + const tenant: string = args.tenant + if (!tenant) { + throw new Error("Must specify tenant's branch key ID for this operation") + } + + return { operation, inFile, outFile, tenant } +} + +// a dummy branch key id supplier which looks for a field with key "branchKeyId" +// inside the EC +class ExampleBranchKeyIdSupplier implements BranchKeyIdSupplier { + getBranchKeyId(encryptionContext: EncryptionContext): string { + return encryptionContext.branchKeyId + } +} + +// configure the keystore +const branchKeyArn = + 'arn:aws:kms:us-west-2:370957321024:key/9d989aa2-2f9c-438c-a745-cc57d3ad0126' + +const keyStore = new BranchKeyStoreNode({ + ddbTableName: 'KeyStoreDdbTable', + logicalKeyStoreName: 'KeyStoreDdbTable', + kmsConfiguration: new SrkCompatibilityKmsConfig(branchKeyArn), +}) + +// function to read input from a file +function readInputData(inFile: string) { + let inData = Buffer.alloc(0) + try { + inData = fs.readFileSync(inFile) + } catch (err) { + console.error(err) + exit(1) + } + + return inData +} + +// a function to write output to a file +function dumpOutputData(outFile: string, outData: Buffer) { + try { + fs.writeFileSync(outFile, outData) + } catch (err) { + console.error(err) + exit(1) + } +} + +const { encrypt, decrypt } = buildClient( + CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT +) + +// function to decrypt with the H-keyring +async function decryptEncryptedData( + encryptedData: Buffer, + keyring: KeyringNode +) { + const { plaintext: decryptedData, messageHeader } = await decrypt( + keyring, + encryptedData + ) + + const { encryptionContext } = messageHeader + + Object.entries(encryptionContext).forEach(([key, value]) => { + if (encryptionContext[key] !== value) { + throw new Error('Encryption Context does not match expected values') + } + }) + + return decryptedData +} + +// function to encrypt with the H-Keyring +async function encryptData( + data: Buffer, + keyring: KeyringNode, + encryptionContext: EncryptionContext +) { + const { result } = await encrypt(keyring, data, { encryptionContext }) + return result +} + +async function main() { + // read cli args + const { operation, inFile, outFile, tenant } = getCliArgs() + // based on CLI tenant arg, find the branch key id + const branchKeyId: string = tenantMap[tenant] + // read input from input file + const inData = readInputData(inFile) + + let outData: Buffer = Buffer.alloc(0) + let msg: string + // if cli arg operation field is encrypt + if (operation === 'encrypt') { + // create a dynamic keyring and encrypt + const keyring = new KmsHierarchicalKeyRingNode({ + branchKeyIdSupplier: new ExampleBranchKeyIdSupplier(), + keyStore, + cacheLimitTtl: 60, + }) + const data = inData + outData = await encryptData(data, keyring, { branchKeyId }) + msg = `Tenant ${tenant} has completed encryption` + } else { + // otherwise, create a static keyring and decrypt + const keyring = new KmsHierarchicalKeyRingNode({ + branchKeyId, + keyStore, + cacheLimitTtl: 60, + }) + const encryptedData = inData + + try { + outData = await decryptEncryptedData(encryptedData, keyring) + } catch { + throw new Error(`Tenant ${tenant} cannot decrypt this encrypted message`) + } + + msg = `Tenant ${tenant} has completed decryption` + } + + // write output to output file + dumpOutputData(outFile, outData) + console.log(msg) +} + +main() diff --git a/modules/example-node/src/kms-hierarchical-keyring/caching_cmm.ts b/modules/example-node/src/kms-hierarchical-keyring/caching_cmm.ts new file mode 100644 index 000000000..1f905c94c --- /dev/null +++ b/modules/example-node/src/kms-hierarchical-keyring/caching_cmm.ts @@ -0,0 +1,170 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + buildClient, + CommitmentPolicy, + NodeCachingMaterialsManager, + getLocalCryptographicMaterialsCache, + BranchKeyStoreNode, + KmsHierarchicalKeyRingNode, +} 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 hKeyringCachingCMMNodeSimpleTest( + keyStoreTableName = 'KeyStoreDdbTable', + logicalKeyStoreName = keyStoreTableName, + kmsKeyId = 'arn:aws:kms:us-west-2:370957321024:key/9d989aa2-2f9c-438c-a745-cc57d3ad0126' +) { + // Configure your KeyStore resource. + // This SHOULD be the same configuration that you used + // to initially create and populate your KeyStore. + const keyStore = new BranchKeyStoreNode({ + storage: {ddbTableName: keyStoreTableName}, + logicalKeyStoreName: logicalKeyStoreName, + kmsConfiguration: { identifier: kmsKeyId }, + }) + + // Here, you would call CreateKey to create an active branch keys + // However, the JS keystore does not currently support this operation, so we + // hard code the ID of an existing active branch key + const branchKeyId = '38853b56-19c6-4345-9cb5-afc2a25dcdd1' + + // Create the Hierarchical Keyring. + const keyring = new KmsHierarchicalKeyRingNode({ + branchKeyId, + keyStore, + cacheLimitTtl: 600, // 10 min + }) + + /* Create a cache to hold the data keys (and related cryptographic material). + * This example uses the local cache provided by the Encryption SDK. + * The `capacity` value represents the maximum number of entries + * that the cache can hold. + * To make room for an additional entry, + * the cache evicts the oldest cached entry. + * Both encrypt and decrypt requests count independently towards this threshold. + * Entries that exceed any cache threshold are actively removed from the cache. + * By default, the SDK checks one item in the cache every 60 seconds (60,000 milliseconds). + * To change this frequency, pass in a `proactiveFrequency` value + * as the second parameter. This value is in milliseconds. + */ + const capacity = 100 + const cache = getLocalCryptographicMaterialsCache(capacity) + + /* The partition name lets multiple caching CMMs share the same local cryptographic cache. + * By default, the entries for each CMM are cached separately. However, if you want these CMMs to share the cache, + * use the same partition name for both caching CMMs. + * If you don't supply a partition name, the Encryption SDK generates a random name for each caching CMM. + * As a result, sharing elements in the cache MUST be an intentional operation. + */ + const partition = 'local partition name' + + /* maxAge is the time in milliseconds that an entry will be cached. + * Elements are actively removed from the cache. + */ + const maxAge = 1000 * 60 + + /* The maximum amount of bytes that will be encrypted under a single data key. + * This value is optional, + * but you should configure the lowest value possible. + */ + const maxBytesEncrypted = 100 + + /* The maximum number of messages that will be encrypted under a single data key. + * This value is optional, + * but you should configure the lowest value possible. + */ + const maxMessagesEncrypted = 10 + + const cachingCMM = new NodeCachingMaterialsManager({ + backingMaterials: keyring, + cache, + partition, + maxAge, + maxBytesEncrypted, + maxMessagesEncrypted, + }) + + /* Encryption context is a *very* powerful tool for controlling + * and managing access. + * When you pass an encryption context to the encrypt function, + * the encryption context is cryptographically bound to the ciphertext. + * If you don't pass in the same encryption context when decrypting, + * the decrypt function fails. + * The encryption context is ***not*** secret! + * Encrypted data is opaque. + * You can use an encryption context to assert things about the encrypted data. + * The encryption context helps you to determine + * whether the ciphertext you retrieved is the ciphertext you expect to decrypt. + * For example, if you are are only expecting data from 'us-west-2', + * the appearance of a different AWS Region in the encryption context can indicate malicious interference. + * See: https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + * + * Also, cached data keys are reused ***only*** when the encryption contexts passed into the functions are an exact case-sensitive match. + * See: https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/data-caching-details.html#caching-encryption-context + */ + const encryptionContext = { + stage: 'demo', + purpose: 'simple demonstration app', + origin: 'us-west-2', + } + + /* Find data to encrypt. A simple string. */ + const cleartext = 'asdf' + + /* Encrypt the data. + * The caching CMM only reuses data keys + * when it know the length (or an estimate) of the plaintext. + * If you do not know the length, + * because the data is a stream + * provide an estimate of the largest expected value. + * + * If your estimate is smaller than the actual plaintext length + * the AWS Encryption SDK will throw an exception. + * + * If the plaintext is not a stream, + * the AWS Encryption SDK uses the actual plaintext length + * instead of any length you provide. + */ + const { result } = await encrypt(cachingCMM, cleartext, { + encryptionContext, + plaintextLength: 4, + }) + + /* Decrypt the data. + * NOTE: This decrypt request will not use the data key + * that was cached during the encrypt operation. + * Data keys for encrypt and decrypt operations are cached separately. + */ + const { plaintext, messageHeader } = await decrypt(cachingCMM, result) + + /* Grab the encryption context so you can verify it. */ + const { encryptionContext: decryptedContext } = 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 include a test that requires that all key-value pairs match. + * Instead, verify that the key-value pairs that you supplied to the `encrypt` function are included in the encryption context that the `decrypt` function returns. + */ + Object.entries(encryptionContext).forEach(([key, value]) => { + if (decryptedContext[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/src/kms-hierarchical-keyring/disable_commitment.ts b/modules/example-node/src/kms-hierarchical-keyring/disable_commitment.ts new file mode 100644 index 000000000..d4b6b28fb --- /dev/null +++ b/modules/example-node/src/kms-hierarchical-keyring/disable_commitment.ts @@ -0,0 +1,93 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + buildClient, + CommitmentPolicy, + BranchKeyStoreNode, + KmsHierarchicalKeyRingNode, +} from '@aws-crypto/client-node' +/* This builds the client with the FORBID_ENCRYPT_ALLOW_DECRYPT commitment policy. + * This configuration should only be used + * as part of a migration + * from version 1.x to 2.x, + * or for advanced users + * with specialized requirements. + * We recommend that AWS Encryption SDK users + * enable commitment whenever possible. + */ +const { encrypt, decrypt } = buildClient( + CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT +) + +export async function hKeyringDisableCommitmentTest( + keyStoreTableName = 'KeyStoreDdbTable', + logicalKeyStoreName = keyStoreTableName, + kmsKeyId = 'arn:aws:kms:us-west-2:370957321024:key/9d989aa2-2f9c-438c-a745-cc57d3ad0126' +) { + // Configure your KeyStore resource. + // This SHOULD be the same configuration that you used + // to initially create and populate your KeyStore. + const keyStore = new BranchKeyStoreNode({ + storage: {ddbTableName: keyStoreTableName}, + logicalKeyStoreName: logicalKeyStoreName, + kmsConfiguration: { identifier: kmsKeyId }, + }) + + // Here, you would call CreateKey to create an active branch keys + // However, the JS keystore does not currently support this operation, so we + // hard code the ID of an existing active branch key + const branchKeyId = '38853b56-19c6-4345-9cb5-afc2a25dcdd1' + + // Create the Hierarchical Keyring. + const keyring = new KmsHierarchicalKeyRingNode({ + branchKeyId, + keyStore, + cacheLimitTtl: 600, // 10 min + }) + + /* 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(keyring, cleartext, { + encryptionContext: context, + }) + + /* Decrypt the data. */ + const { plaintext, messageHeader } = await decrypt(keyring, 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/src/kms-hierarchical-keyring/multi_keyring.ts b/modules/example-node/src/kms-hierarchical-keyring/multi_keyring.ts new file mode 100644 index 000000000..d242da425 --- /dev/null +++ b/modules/example-node/src/kms-hierarchical-keyring/multi_keyring.ts @@ -0,0 +1,133 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +/* This is a simple example of using a multi-keyring KMS keyring + * to combine a KMS keyring and a raw AES keyring + * to encrypt and decrypt using the AWS Encryption SDK for Javascript in Node.js. + */ + +import { + MultiKeyringNode, + RawAesKeyringNode, + RawAesWrappingSuiteIdentifier, + buildClient, + CommitmentPolicy, + BranchKeyStoreNode, + KmsHierarchicalKeyRingNode, +} from '@aws-crypto/client-node' +import { randomBytes } from 'crypto' + +/* 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 hierarchicalAesMultiKeyringTest( + keyStoreTableName = 'KeyStoreDdbTable', + logicalKeyStoreName = keyStoreTableName, + kmsKeyId = 'arn:aws:kms:us-west-2:370957321024:key/9d989aa2-2f9c-438c-a745-cc57d3ad0126' +) { + // Configure your KeyStore resource. + // This SHOULD be the same configuration that you used + // to initially create and populate your KeyStore. + const keyStore = new BranchKeyStoreNode({ + storage: {ddbTableName: keyStoreTableName}, + logicalKeyStoreName: logicalKeyStoreName, + kmsConfiguration: { identifier: kmsKeyId }, + }) + + // Here, you would call CreateKey to create an active branch keys + // However, the JS keystore does not currently support this operation, so we + // hard code the ID of an existing active branch key + const branchKeyId = '38853b56-19c6-4345-9cb5-afc2a25dcdd1' + + // Create the Hierarchical Keyring. + const kmsHKerying = new KmsHierarchicalKeyRingNode({ + branchKeyId, + keyStore, + cacheLimitTtl: 600, // 10 min + }) + + /* You need to specify a name + * and a namespace for raw encryption key providers. + * The name and namespace that you use in the decryption keyring *must* be an exact, + * *case-sensitive* match for the name and namespace in the encryption keyring. + */ + const keyName = 'aes-name' + const keyNamespace = 'aes-namespace' + /* The wrapping suite defines the AES-GCM algorithm suite to use. */ + const wrappingSuite = + RawAesWrappingSuiteIdentifier.AES256_GCM_IV12_TAG16_NO_PADDING + // Get your plaintext master key from wherever you store it. + const unencryptedMasterKey = randomBytes(32) + + /* Configure the Raw AES Keyring. */ + const aesKeyring = new RawAesKeyringNode({ + keyName, + keyNamespace, + unencryptedMasterKey, + wrappingSuite, + }) + + /* Combine the two keyrings with a MultiKeyring. */ + const keyring = new MultiKeyringNode({ + generator: kmsHKerying, + children: [aesKeyring], + }) + + /* 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(keyring, cleartext, { + encryptionContext: context, + }) + + /* Decrypt the data. + * This decrypt call could be done with **any** of the 3 keyrings. + * Here we use the multi-keyring, but + * decrypt(kmsHKeyring, result) + * decrypt(aesKeyring, result) + * would both work as well. + */ + const { plaintext, messageHeader } = await decrypt(keyring, 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/src/kms-hierarchical-keyring/multi_tenancy.ts b/modules/example-node/src/kms-hierarchical-keyring/multi_tenancy.ts new file mode 100644 index 000000000..5dd1cea6a --- /dev/null +++ b/modules/example-node/src/kms-hierarchical-keyring/multi_tenancy.ts @@ -0,0 +1,217 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + BranchKeyStoreNode, + buildClient, + CommitmentPolicy, + KmsHierarchicalKeyRingNode, + BranchKeyIdSupplier, + EncryptionContext, +} from '@aws-crypto/client-node' + +/** + * This example sets up the Hierarchical Keyring, which establishes a key hierarchy where "branch" + * keys are persisted in DynamoDb. These branch keys are used to protect your data keys, and these + * branch keys are themselves protected by a KMS Key. + * + * Establishing a key hierarchy like this has two benefits: + * + * First, by caching the branch key material, and only calling KMS to re-establish authentication + * regularly according to your configured TTL, you limit how often you need to call KMS to protect + * your data. This is a performance security tradeoff, where your authentication, audit, and logging + * from KMS is no longer one-to-one with every encrypt or decrypt call. Additionally, KMS Cloudtrail + * cannot be used to distinguish Encrypt and Decrypt calls, and you cannot restrict who has + * Encryption rights from who has Decryption rights since they both ONLY need KMS:Decrypt. However, + * the benefit is that you no longer have to make a network call to KMS for every encrypt or + * decrypt. + * + * Second, this key hierarchy facilitates cryptographic isolation of a tenant's data in a + * multi-tenant data store. Each tenant can have a unique Branch Key, that is only used to protect + * the tenant's data. You can either statically configure a single branch key to ensure you are + * restricting access to a single tenant, or you can implement an interface that selects the Branch + * Key based on the Encryption Context. + * + * This example demonstrates configuring a Hierarchical Keyring with a Branch Key ID Supplier to + * encrypt and decrypt data for two separate tenants. + * + * This example requires access to the DDB Table where you are storing the Branch Keys. This + * table must be configured with the following primary key configuration: - Partition key is named + * "partition_key" with type (S) - Sort key is named "sort_key" with type (S) + * + * This example also requires using a KMS Key. You need the following access on this key: - + * GenerateDataKeyWithoutPlaintext - Decrypt + */ + +/* 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 +) + +// Implement an example branch key id supplier +// Use the encryption contexts to define friendly names for each branch key +class ExampleBranchKeyIdSupplier implements BranchKeyIdSupplier { + private _branchKeyIdForTenantA: string + private _branchKeyIdForTenantB: string + + constructor(tenant1Id: string, tenant2Id: string) { + this._branchKeyIdForTenantA = tenant1Id + this._branchKeyIdForTenantB = tenant2Id + } + + getBranchKeyId(encryptionContext: EncryptionContext): string { + if ('tenant' in encryptionContext === false) { + throw new Error( + 'EncryptionContext invalid, does not contain expected tenant key value pair.' + ) + } + + const tenantKeyId = encryptionContext['tenant'] + let branchKeyId: string + + if (tenantKeyId === 'TenantA') { + branchKeyId = this._branchKeyIdForTenantA + } else if (tenantKeyId === 'TenantB') { + branchKeyId = this._branchKeyIdForTenantB + } else { + throw new Error('Item does not contain valid tenant ID') + } + + return branchKeyId + } +} + +export async function hKeyringMultiTenancy( + keyStoreTableName = 'KeyStoreDdbTable', + logicalKeyStoreName = keyStoreTableName, + kmsKeyId = 'arn:aws:kms:us-west-2:370957321024:key/9d989aa2-2f9c-438c-a745-cc57d3ad0126' +) { + // Configure your KeyStore resource. + // This SHOULD be the same configuration that you used + // to initially create and populate your KeyStore. + const keyStore = new BranchKeyStoreNode({ + storage: {ddbTableName: keyStoreTableName}, + logicalKeyStoreName: logicalKeyStoreName, + kmsConfiguration: { identifier: kmsKeyId }, + }) + + // Here, you would call CreateKey to create two new active branch keys. + // However, the JS keystore does not currently support this operation, so we + // hard code the IDs of two existing active branch keys + const branchKeyIdA = '38853b56-19c6-4345-9cb5-afc2a25dcdd1' + const branchKeyIdB = '2c583585-5770-467d-8f59-b346d0ed1994' + + // Create a branch key supplier that maps the branch key id to a more readable format + const branchKeyIdSupplier = new ExampleBranchKeyIdSupplier( + branchKeyIdA, + branchKeyIdB + ) + + // Create the Hierarchical Keyring. + const keyring = new KmsHierarchicalKeyRingNode({ + branchKeyIdSupplier, + keyStore, + cacheLimitTtl: 600, // 10 min + }) + + // The Branch Key Id supplier uses the encryption context to determine which branch key id will + // be used to encrypt data. + // Create encryption context for TenantA + const encryptionContextAIn = { + tenant: 'TenantA', + encryption: 'context', + 'is not': 'secret', + 'but adds': 'useful metadata', + 'that can help you': 'be confident that', + 'the data you are handling': 'is what you think it is', + } + + // Create encryption context for TenantB + const encryptionContextBIn = { + tenant: 'TenantB', + encryption: 'context', + 'is not': 'secret', + 'but adds': 'useful metadata', + 'that can help you': 'be confident that', + 'the data you are handling': 'is what you think it is', + } + + /* Find data to encrypt. A simple string. */ + const cleartext = 'asdf' + + // Encrypt the data for encryptionContextA & encryptionContextB + const { result: encryptResultA } = await encrypt(keyring, cleartext, { + encryptionContext: encryptionContextAIn, + }) + const { result: encryptResultB } = await encrypt(keyring, cleartext, { + encryptionContext: encryptionContextBIn, + }) + + // To attest that TenantKeyB cannot decrypt a message written by TenantKeyA + // let's construct more restrictive hierarchical keyrings. + const keyringA = new KmsHierarchicalKeyRingNode({ + branchKeyId: branchKeyIdA, + keyStore, + cacheLimitTtl: 600, + }) + + const keyringB = new KmsHierarchicalKeyRingNode({ + branchKeyId: branchKeyIdB, + keyStore, + cacheLimitTtl: 600, + }) + + let decryptAFailed = false + // Try to use keyring for Tenant B to decrypt a message encrypted with Tenant A's key + // Expected to fail. + try { + await decrypt(keyringB, encryptResultA) + } catch (e) { + decryptAFailed = true + } + + let decryptBFailed = false + // Try to use keyring for Tenant A to decrypt a message encrypted with Tenant B's key + // Expected to fail. + try { + await decrypt(keyringA, encryptResultB) + } catch (e) { + decryptBFailed = true + } + + // we will assert that both decrypts failed + const decryptsFailed = decryptAFailed && decryptBFailed + + // Decrypt your encrypted data using the same keyring you used on encrypt. + + const { plaintext: plaintextA, messageHeader: messageHeaderA } = + await decrypt(keyring, encryptResultA) + /* Grab the encryption context so you can verify it. */ + const { encryptionContext: encryptionContextAOut } = messageHeaderA + Object.entries(encryptionContextAIn).forEach(([key, value]) => { + if (encryptionContextAOut[key] !== value) + throw new Error('Encryption Context does not match expected values') + }) + + const { plaintext: plaintextB, messageHeader: messageHeaderB } = + await decrypt(keyring, encryptResultB) + /* Grab the encryption context so you can verify it. */ + const { encryptionContext: encryptionContextBOut } = messageHeaderB + Object.entries(encryptionContextBIn).forEach(([key, value]) => { + if (encryptionContextBOut[key] !== value) + throw new Error('Encryption Context does not match expected values') + }) + + // we will assert that both decrypted plaintexts are the same as the original + // cleartext + + /* Return the values so the code can be tested. */ + return { decryptsFailed, cleartext, plaintextA, plaintextB } +} diff --git a/modules/example-node/src/kms-hierarchical-keyring/simple.ts b/modules/example-node/src/kms-hierarchical-keyring/simple.ts new file mode 100644 index 000000000..91eedb217 --- /dev/null +++ b/modules/example-node/src/kms-hierarchical-keyring/simple.ts @@ -0,0 +1,125 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + BranchKeyStoreNode, + buildClient, + CommitmentPolicy, + KmsHierarchicalKeyRingNode, +} from '@aws-crypto/client-node' + +/** + * This example sets up the Hierarchical Keyring, which establishes a key hierarchy where "branch" + * keys are persisted in DynamoDb. These branch keys are used to protect your data keys, and these + * branch keys are themselves protected by a KMS Key. + * + * Establishing a key hierarchy like this has two benefits: + * + * First, by caching the branch key material, and only calling KMS to re-establish authentication + * regularly according to your configured TTL, you limit how often you need to call KMS to protect + * your data. This is a performance security tradeoff, where your authentication, audit, and logging + * from KMS is no longer one-to-one with every encrypt or decrypt call. Additionally, KMS Cloudtrail + * cannot be used to distinguish Encrypt and Decrypt calls, and you cannot restrict who has + * Encryption rights from who has Decryption rights since they both ONLY need KMS:Decrypt. However, + * the benefit is that you no longer have to make a network call to KMS for every encrypt or + * decrypt. + * + * Second, this key hierarchy facilitates cryptographic isolation of a tenant's data in a + * multi-tenant data store. Each tenant can have a unique Branch Key, that is only used to protect + * the tenant's data. You can either statically configure a single branch key to ensure you are + * restricting access to a single tenant, or you can implement an interface that selects the Branch + * Key based on the Encryption Context. + * + * This example demonstrates statically configuring a Hierarchical Keyring with + * a single branch key to showcase how access can be restricted to a single tenant. + * + * This example requires access to the DDB Table where you are storing the Branch Keys. This + * table must be configured with the following primary key configuration: - Partition key is named + * "partition_key" with type (S) - Sort key is named "sort_key" with type (S) + * + * This example also requires using a KMS Key. You need the following access on this key: - + * GenerateDataKeyWithoutPlaintext - Decrypt + */ + +/* 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 hKeyringSimpleTest( + keyStoreTableName = 'KeyStoreDdbTable', + logicalKeyStoreName = keyStoreTableName, + kmsKeyId = 'arn:aws:kms:us-west-2:370957321024:key/9d989aa2-2f9c-438c-a745-cc57d3ad0126' +) { + // Configure your KeyStore resource. + // This SHOULD be the same configuration that you used + // to initially create and populate your KeyStore. + const keyStore = new BranchKeyStoreNode({ + storage: {ddbTableName: keyStoreTableName}, + logicalKeyStoreName: logicalKeyStoreName, + kmsConfiguration: { identifier: kmsKeyId }, + }) + + // Here, you would call CreateKey to create an active branch keys + // However, the JS keystore does not currently support this operation, so we + // hard code the ID of an existing active branch key + const branchKeyId = '38853b56-19c6-4345-9cb5-afc2a25dcdd1' + + // Create the Hierarchical Keyring. + const keyring = new KmsHierarchicalKeyRingNode({ + branchKeyId, + keyStore, + cacheLimitTtl: 600, // 10 min + }) + + /* 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(keyring, cleartext, { + encryptionContext: context, + }) + + /* Decrypt the data. */ + const { plaintext, messageHeader } = await decrypt(keyring, 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/src/kms-hierarchical-keyring/stream.ts b/modules/example-node/src/kms-hierarchical-keyring/stream.ts new file mode 100644 index 000000000..90309b73e --- /dev/null +++ b/modules/example-node/src/kms-hierarchical-keyring/stream.ts @@ -0,0 +1,126 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + buildClient, + CommitmentPolicy, + MessageHeader, + BranchKeyStoreNode, + KmsHierarchicalKeyRingNode, +} from '@aws-crypto/client-node' +import { AlgorithmSuiteIdentifier } from '@aws-crypto/material-management' + +/* 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 { encryptStream, decryptUnsignedMessageStream } = buildClient( + CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT +) + +import { finished } from 'stream' +import { createReadStream } from 'fs' +import { promisify } from 'util' +const finishedAsync = promisify(finished) + +export async function hKeyringStreamTest( + filename: string, + keyStoreTableName = 'KeyStoreDdbTable', + logicalKeyStoreName = keyStoreTableName, + kmsKeyId = 'arn:aws:kms:us-west-2:370957321024:key/9d989aa2-2f9c-438c-a745-cc57d3ad0126' +) { + // Configure your KeyStore resource. + // This SHOULD be the same configuration that you used + // to initially create and populate your KeyStore. + const keyStore = new BranchKeyStoreNode({ + storage: {ddbTableName: keyStoreTableName}, + logicalKeyStoreName: logicalKeyStoreName, + kmsConfiguration: { identifier: kmsKeyId }, + }) + + // Here, you would call CreateKey to create an active branch keys + // However, the JS keystore does not currently support this operation, so we + // hard code the ID of an existing active branch key + const branchKeyId = '38853b56-19c6-4345-9cb5-afc2a25dcdd1' + + // Create the Hierarchical Keyring. + const keyring = new KmsHierarchicalKeyRingNode({ + branchKeyId, + keyStore, + cacheLimitTtl: 600, // 10 min + }) + + /* 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', + } + + /* Create a simple pipeline to encrypt the package.json for this project. */ + const stream = createReadStream(filename) + .pipe( + encryptStream(keyring, { + /* + * Since we are streaming, and assuming that the encryption and decryption contexts + * are equally trusted, using an unsigned algorithm suite is faster and avoids + * the possibility of processing plaintext before the signature is verified. + */ + suiteId: + AlgorithmSuiteIdentifier.ALG_AES256_GCM_IV12_TAG16_HKDF_SHA512_COMMIT_KEY, + encryptionContext: context, + }) + ) + /* + * decryptUnsignedMessageStream is recommended when streaming if you don't need + * digital signatures. + */ + .pipe( + decryptUnsignedMessageStream( + new KmsHierarchicalKeyRingNode({ + branchKeyId, + keyStore, + cacheLimitTtl: 600, + }) + ) + ) + .on('MessageHeader', ({ encryptionContext }: MessageHeader) => { + /* Verify the encryption context. + * Depending on the Algorithm Suite, the `encryptionContext` _may_ contain additional values. + * In Signing Algorithm Suites the public verification key is serialized into the `encryptionContext`. + * 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') + }) + }) + + /* This is not strictly speaking part of the example. + * Streams need a place to drain. + * To test this code I just accumulate the stream. + * Then I can return that Buffer and verify. + * In a real world case you do not want to always buffer the whole stream. + */ + const buff: Buffer[] = [] + stream.on('data', (chunk: Buffer) => { + buff.push(chunk) + }) + + await finishedAsync(stream) + return Buffer.concat(buff) +} diff --git a/modules/example-node/test/index.test.ts b/modules/example-node/test/index.test.ts index 75f73ab2c..2e8538420 100644 --- a/modules/example-node/test/index.test.ts +++ b/modules/example-node/test/index.test.ts @@ -3,7 +3,7 @@ /* eslint-env mocha */ -import { expect } from 'chai' +import chai, { expect } from 'chai' import { rsaTest } from '../src/rsa_simple' import { kmsSimpleTest } from '../src/kms_simple' import { kmsStreamTest } from '../src/kms_stream' @@ -18,8 +18,58 @@ import { import { kmsMultiRegionSimpleTest } from '../src/kms_multi_region_simple' import { kmsMultiRegionDiscoveryTest } from '../src/kms_multi_region_discovery' import { readFileSync } from 'fs' - +import { hKeyringSimpleTest } from '../src/kms-hierarchical-keyring/simple' +import { hKeyringMultiTenancy } from '../src/kms-hierarchical-keyring/multi_tenancy' +import { hKeyringCachingCMMNodeSimpleTest } from '../src/kms-hierarchical-keyring/caching_cmm' +import chaiAsPromised from 'chai-as-promised' +import { hKeyringDisableCommitmentTest } from '../src/kms-hierarchical-keyring/disable_commitment' +import { hKeyringStreamTest } from '../src/kms-hierarchical-keyring/stream' +import { hierarchicalAesMultiKeyringTest } from '../src/kms-hierarchical-keyring/multi_keyring' + +chai.use(chaiAsPromised) describe('test', () => { + describe('AWS KMS Hierarchical Keyring', () => { + it('Simple', async () => { + const { cleartext, plaintext } = await hKeyringSimpleTest() + + expect(plaintext.toString()).to.equal(cleartext) + }) + + it('Multi-tenancy', async () => { + const { decryptsFailed, cleartext, plaintextA, plaintextB } = + await hKeyringMultiTenancy() + + expect(decryptsFailed).to.be.true + expect(plaintextA.toString()).to.equal(cleartext) + expect(plaintextB.toString()).to.equal(cleartext) + }) + + it('Caching CMM node', async () => { + const { cleartext, plaintext } = await hKeyringCachingCMMNodeSimpleTest() + + expect(plaintext.toString()).to.equal(cleartext) + }) + + it('disableCommitmentTest', async () => { + const { cleartext, plaintext } = await hKeyringDisableCommitmentTest() + + expect(plaintext.toString()).to.equal(cleartext) + }) + + it('Stream', async () => { + const test = await hKeyringStreamTest(__filename) + const clearFile = readFileSync(__filename) + + expect(test).to.deep.equal(clearFile) + }) + + it('Multi Keyring', async () => { + const { cleartext, plaintext } = await hierarchicalAesMultiKeyringTest() + + expect(plaintext.toString()).to.equal(cleartext) + }) + }) + it('rsa', async () => { const { cleartext, plaintext } = await rsaTest() diff --git a/modules/kdf-ctr-mode-node/.eslintrc.js b/modules/kdf-ctr-mode-node/.eslintrc.js new file mode 100644 index 000000000..1c61b83da --- /dev/null +++ b/modules/kdf-ctr-mode-node/.eslintrc.js @@ -0,0 +1,12 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +module.exports = { + parserOptions: { + // There is an issue with @typescript-eslint/parser performance. + // It scales with the number of projects + // see https://github.com/typescript-eslint/typescript-eslint/issues/1192#issuecomment-596741806 + project: '../../tsconfig.lint.json', + tsconfigRootDir: __dirname, + } +} diff --git a/modules/kdf-ctr-mode-node/CHANGELOG.md b/modules/kdf-ctr-mode-node/CHANGELOG.md new file mode 100644 index 000000000..1613e6ab7 --- /dev/null +++ b/modules/kdf-ctr-mode-node/CHANGELOG.md @@ -0,0 +1,59 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [4.0.0](https://github.com/aws/aws-encryption-sdk-javascript/compare/v3.2.2...v4.0.0) (2023-07-17) + +**Note:** Version bump only for package @aws-crypto/hkdf-node + +# [3.0.0](https://github.com/aws/aws-encryption-sdk-javascript/compare/v2.4.0...v3.0.0) (2021-07-14) + +**Note:** Version bump only for package @aws-crypto/hkdf-node + +# [2.4.0](https://github.com/aws/aws-encryption-sdk-javascript/compare/v2.3.1...v2.4.0) (2021-07-13) + +**Note:** Version bump only for package @aws-crypto/hkdf-node + +# [2.2.0](https://github.com/aws/private-aws-encryption-sdk-javascript-staging/compare/@aws-crypto/hkdf-node@1.0.3...@aws-crypto/hkdf-node@2.2.0) (2021-05-27) + +**Note:** Version bump only for package @aws-crypto/hkdf-node + +## [1.0.3](https://github.com/aws/aws-encryption-sdk-javascript/compare/@aws-crypto/hkdf-node@1.0.2...@aws-crypto/hkdf-node@1.0.3) (2020-05-26) + +**Note:** Version bump only for package @aws-crypto/hkdf-node + +## [1.0.2](https://github.com/aws/aws-encryption-sdk-javascript/compare/@aws-crypto/hkdf-node@1.0.1...@aws-crypto/hkdf-node@1.0.2) (2020-04-02) + +**Note:** Version bump only for package @aws-crypto/hkdf-node + +## [1.0.1](/compare/@aws-crypto/hkdf-node@1.0.0...@aws-crypto/hkdf-node@1.0.1) (2020-02-07) + +### Bug Fixes + +- lerna version maintains package-lock (#235) c901318, closes #235 #234 + +# [1.0.0](/compare/@aws-crypto/hkdf-node@0.2.0-preview.1...@aws-crypto/hkdf-node@1.0.0) (2019-10-01) + +**Note:** Version bump only for package @aws-crypto/hkdf-node + +# [0.2.0-preview.1](/compare/@aws-crypto/hkdf-node@0.2.0-preview.0...@aws-crypto/hkdf-node@0.2.0-preview.1) (2019-06-21) + +### Bug Fixes + +- package.json files path update (#120) fbc3270, closes #120 + +# 0.2.0-preview.0 (2019-06-21) + +### Bug Fixes + +- dependencies and lint (#75) 5324491, closes #75 +- hkdf lint 3eed8dd +- LICENSE file needs date and owner e0f7085 +- Update nyc version fcfa3af + +### Features + +- Adding all test vectors from C ESDK (#78) 44ec53c, closes #78 #32 +- Better language for hkdf-node README (#112) 16951b0, closes #112 +- HKDF initial commit (#4) bd83211, closes #4 diff --git a/modules/kdf-ctr-mode-node/LICENSE b/modules/kdf-ctr-mode-node/LICENSE new file mode 100644 index 000000000..304985bfd --- /dev/null +++ b/modules/kdf-ctr-mode-node/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/modules/kdf-ctr-mode-node/NOTICE b/modules/kdf-ctr-mode-node/NOTICE new file mode 100644 index 000000000..d8bf146c7 --- /dev/null +++ b/modules/kdf-ctr-mode-node/NOTICE @@ -0,0 +1,5 @@ +AWS SDK for JavaScript +Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +This product includes software developed at +Amazon Web Services, Inc. (http://aws.amazon.com/). \ No newline at end of file diff --git a/modules/kdf-ctr-mode-node/README.md b/modules/kdf-ctr-mode-node/README.md new file mode 100644 index 000000000..6b89b9302 --- /dev/null +++ b/modules/kdf-ctr-mode-node/README.md @@ -0,0 +1,36 @@ +# @aws-crypto/kdf-ctr-mode-node + +This module exports a Key Derivation Function in Counter Mode with a Pseudo +Random function with HMAC SHA 256 for Node.js. + +This module is used in the the AWS Encryption SDK for JavaScript +to provide key derivation for specific algorithm suites. + +Specification: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-108r1.pdf + +## install + +```sh +npm install @aws-crypto/kdf-ctr-mode-node +``` + +## use + +```javascript +const HKDF = require('@aws-crypto/hkdf-node') +const expand = HKDF('sha256')('some key', 'some salt') +const info = { some: 'info', message_id: 123 } +const key = expand(32, Buffer.from(JSON.stringify(info))) +``` + +## test + +```sh +npm test +``` + +## license + +This SDK is distributed under the +[Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0), +see LICENSE.txt and NOTICE.txt for more information. diff --git a/modules/kdf-ctr-mode-node/package.json b/modules/kdf-ctr-mode-node/package.json new file mode 100644 index 000000000..4e425ff28 --- /dev/null +++ b/modules/kdf-ctr-mode-node/package.json @@ -0,0 +1,42 @@ +{ + "name": "@aws-crypto/kdf-ctr-mode-node", + "version": "4.0.0", + "description": "nodejs kdf ctr mode crypto primitive", + "scripts": { + "prepublishOnly": "npm run build", + "build": "tsc -b tsconfig.json && tsc -b tsconfig.module.json", + "lint": "run-s lint-*", + "lint-eslint": "eslint src/*.ts test/**/*.ts", + "lint-prettier": "prettier -c src/*.ts test/**/*.ts", + "mocha": "mocha --require ts-node/register test/**/*test.ts", + "test": "npm run lint && npm run coverage", + "coverage": "nyc -e .ts npm run mocha" + }, + "repository": "", + "author": { + "name": "AWS Crypto Tools Team", + "email": "aws-cryptools@amazon.com", + "url": "https://docs.aws.amazon.com/aws-crypto-tools/index.html?id=docs_gateway#lang/en_us" + }, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.2.0" + }, + "sideEffects": false, + "main": "./build/main/src/index.js", + "module": "./build/module/src/index.js", + "types": "./build/main/src/index.d.ts", + "files": [ + "build/**/src/*" + ], + "standard": { + "fix": true, + "parser": "@typescript-eslint/parser", + "plugins": [ + "@typescript-eslint" + ] + }, + "devDependencies": { + "@types/sinon": "^17.0.3" + } +} diff --git a/modules/kdf-ctr-mode-node/src/index.ts b/modules/kdf-ctr-mode-node/src/index.ts new file mode 100644 index 000000000..f7b2d0c6e --- /dev/null +++ b/modules/kdf-ctr-mode-node/src/index.ts @@ -0,0 +1,4 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +export { kdfCounterMode } from './kdfctr' diff --git a/modules/kdf-ctr-mode-node/src/kdfctr.ts b/modules/kdf-ctr-mode-node/src/kdfctr.ts new file mode 100644 index 000000000..a0f5d937f --- /dev/null +++ b/modules/kdf-ctr-mode-node/src/kdfctr.ts @@ -0,0 +1,157 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +/* + * Implementation of the https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-108r1.pdf + * Key Derivation in Counter Mode Using Pseudorandom Functions. This + * implementation mirrors the Dafny one: https://github.com/aws/aws-cryptographic-material-providers-library/blob/main/AwsCryptographyPrimitives/src/KDF/KdfCtr.dfy + */ + +import { createHash, createHmac } from 'crypto' +import { needs } from '@aws-crypto/material-management' +import { uInt32BE } from '@aws-crypto/serialize' + +const SEPARATION_INDICATOR = Buffer.from([0x00]) +const COUNTER_START_VALUE = 1 +export const INT32_MAX_LIMIT = 2147483647 +const SUPPORTED_IKM_LENGTHS = [32] +const SUPPORTED_NONCE_LENGTHS = [16] +const SUPPORTED_DERIVED_KEY_LENGTHS = [32] +const SUPPORTED_DIGEST_ALGORITHMS = ['sha256'] + +export type SupportedDigestAlgorithms = 'sha256' +export type SupportedDerivedKeyLengths = 32 + +interface KdfCtrInput { + digestAlgorithm: SupportedDigestAlgorithms + ikm: Buffer + nonce?: Buffer + purpose?: Buffer + expectedLength: SupportedDerivedKeyLengths +} + +export function kdfCounterMode({ + digestAlgorithm, + ikm, + nonce, + purpose, + expectedLength, +}: KdfCtrInput): Buffer { + /* Precondition: the ikm must be 32 bytes long */ + needs( + SUPPORTED_IKM_LENGTHS.includes(ikm.length), + `Unsupported IKM length ${ikm.length}` + ) + /* Precondition: the nonce is required */ + needs(nonce, 'The nonce must be provided') + /* Precondition: the nonce must be 16 bytes long */ + needs( + SUPPORTED_NONCE_LENGTHS.includes(nonce.length), + `Unsupported nonce length ${nonce.length}` + ) + /* Precondition: the expected length must be 32 bytes */ + /* Precondition: the expected length * 8 must be under the max 32-bit signed integer */ + needs( + SUPPORTED_DERIVED_KEY_LENGTHS.includes(expectedLength) && + expectedLength * 8 < INT32_MAX_LIMIT && + expectedLength * 8 > 0, + `Unsupported requested length ${expectedLength}` + ) + + const label = purpose || Buffer.alloc(0) + const info = nonce || Buffer.alloc(0) + const internalLength = 8 + SEPARATION_INDICATOR.length + + /* Precondition: the input length must be under the max 32-bit signed integer */ + needs( + internalLength + label.length + info.length < INT32_MAX_LIMIT, + `Input Length ${ + internalLength + label.length + info.length + } must be under ${INT32_MAX_LIMIT} bytes` + ) + + const lengthBits = Buffer.from(uInt32BE(expectedLength * 8)) + const explicitInfo = Buffer.concat([ + label, + SEPARATION_INDICATOR, + info, + lengthBits, + ]) + + return rawDerive(ikm, explicitInfo, expectedLength, digestAlgorithm) +} + +export function rawDerive( + ikm: Buffer, + explicitInfo: Buffer, + length: number, + // omit offset as a parameter because it is unused, causing compile errors due + // to configured project settings + digestAlgorithm: SupportedDigestAlgorithms +): Buffer { + const h = createHash(digestAlgorithm).digest().length + + /* Precondition: expected length must be positive */ + needs(length > 0, `Requested length ${length} must be positive`) + /* Precondition: length of explicit info + 4 bytes should be under the max 32-bit signed integer */ + needs( + 4 + explicitInfo.length < INT32_MAX_LIMIT, + `Explicit info length ${explicitInfo.length} must be under ${ + INT32_MAX_LIMIT - 4 + } bytes` + ) + /* Precondition: the digest algorithm should be sha256 */ + needs( + SUPPORTED_DIGEST_ALGORITHMS.includes(digestAlgorithm), + `Unsupported digest algorithm ${digestAlgorithm}` + ) + /* Precondition: the expected length + digest hash length should be under the max 32-bit signed integer - 1 */ + needs( + length + h < INT32_MAX_LIMIT - 1, + `The combined requested and digest hash length ${ + length + h + } must be under ${INT32_MAX_LIMIT - 1} bytes` + ) + + // number of iterations calculated in accordance with SP800-108 + const iterations = Math.ceil(length / h) + let buffer = Buffer.alloc(0) + let i = Buffer.from(uInt32BE(COUNTER_START_VALUE)) + + for (let iteration = 1; iteration <= iterations + 1; iteration++) { + const digest = createHmac(digestAlgorithm, ikm) + .update(i) + .update(explicitInfo) + .digest() + buffer = Buffer.concat([buffer, digest]) + i = increment(i) + } + + needs(buffer.length >= length, 'Failed to derive key of requested length') + return buffer.subarray(0, length) +} + +export function increment(x: Buffer): Buffer { + /* Precondition: buffer length must be 4 bytes */ + needs(x.length === 4, `Buffer length ${x.length} must be 4 bytes`) + + let output: Buffer + if (x[3] < 255) { + output = Buffer.from([x[0], x[1], x[2], x[3] + 1]) + } else if (x[2] < 255) { + output = Buffer.from([x[0], x[1], x[2] + 1, 0]) + } else if (x[1] < 255) { + output = Buffer.from([x[0], x[1] + 1, 0, 0]) + } else if (x[0] < 255) { + output = Buffer.from([x[0] + 1, 0, 0, 0]) + } else { + throw new Error('Unable to derive key material; may have exceeded limit.') + } + + /* Postcondition: incremented buffer length must be 4 bytes */ + needs( + output.length === 4, + `Incremented buffer length ${output.length} must be 4 bytes` + ) + return output +} diff --git a/modules/kdf-ctr-mode-node/test/kdfctr.test.ts b/modules/kdf-ctr-mode-node/test/kdfctr.test.ts new file mode 100644 index 000000000..10ab7a8e1 --- /dev/null +++ b/modules/kdf-ctr-mode-node/test/kdfctr.test.ts @@ -0,0 +1,311 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { expect } from 'chai' +import { + INT32_MAX_LIMIT, + increment, + kdfCounterMode, + rawDerive, + SupportedDigestAlgorithms, + SupportedDerivedKeyLengths, +} from '../src/kdfctr' +import { rawTestVectors, testVectors } from './testvectors' +import { createHash } from 'crypto' + +describe('KDF Ctr Mode', () => { + const ikm = Buffer.alloc(32) + const nonce = Buffer.alloc(16) + const digestAlgorithm = 'sha256' + const expectedLength = 32 + const purpose = Buffer.from('aws-kms-hierarchy', 'utf-8') + + it('Purpose is optional', () => + expect(() => + kdfCounterMode({ + digestAlgorithm, + ikm, + nonce, + purpose: undefined, + expectedLength, + }) + ).to.not.throw()) + + it('Precondition: the nonce is required', () => { + expect(() => + kdfCounterMode({ + digestAlgorithm, + ikm, + nonce: undefined, + purpose, + expectedLength, + }) + ).to.throw('The nonce must be provided') + }) + + it('Precondition: the ikm must be 32 bytes long', () => { + const invalidIkm = Buffer.alloc(31) + expect(() => + kdfCounterMode({ + digestAlgorithm, + ikm: invalidIkm, + nonce, + purpose, + expectedLength, + }) + ).to.throw(`Unsupported IKM length ${invalidIkm.length}`) + + expect(() => + kdfCounterMode({ + digestAlgorithm, + ikm: Buffer.alloc(32), + nonce, + purpose, + expectedLength, + }) + ).to.not.throw() + }) + + it('Precondition: the nonce must be 16 bytes long', () => { + const invalidNonce = Buffer.alloc(17) + expect(() => + kdfCounterMode({ + digestAlgorithm, + ikm, + nonce: invalidNonce, + purpose, + expectedLength, + }) + ).to.throw(`Unsupported nonce length ${invalidNonce.length}`) + + expect(() => + kdfCounterMode({ + digestAlgorithm, + ikm, + nonce: Buffer.alloc(16), + purpose, + expectedLength, + }) + ).to.not.throw() + }) + + it('Precondition: the expected length must be 32 bytes', () => { + const invalidExpectedLength = 31 as SupportedDerivedKeyLengths + expect(() => + kdfCounterMode({ + digestAlgorithm, + ikm, + nonce, + purpose, + expectedLength: invalidExpectedLength, + }) + ).to.throw(`Unsupported requested length ${invalidExpectedLength}`) + + expect(() => + kdfCounterMode({ + digestAlgorithm, + ikm, + nonce, + purpose, + expectedLength: 32, + }) + ).to.not.throw() + }) + + it('Precondition: the expected length * 8 must be under the max 32-bit signed integer', () => { + let invalidExpectedLength = (INT32_MAX_LIMIT / + 8) as SupportedDerivedKeyLengths + expect(() => + kdfCounterMode({ + digestAlgorithm, + ikm, + nonce, + purpose, + expectedLength: invalidExpectedLength, + }) + ).to.throw(`Unsupported requested length ${invalidExpectedLength}`) + + invalidExpectedLength = -60 as SupportedDerivedKeyLengths + expect(() => + kdfCounterMode({ + digestAlgorithm, + ikm, + nonce, + purpose, + expectedLength: invalidExpectedLength, + }) + ).to.throw(`Unsupported requested length ${invalidExpectedLength}`) + }) + + it('Precondition: the input length must be under the max 32-bit signed integer', () => { + const invalidPurpose = Buffer.alloc( + INT32_MAX_LIMIT - (4 + 4 + 1 + nonce.length) + ) + expect(() => + kdfCounterMode({ + digestAlgorithm, + ikm, + nonce, + purpose: invalidPurpose, + expectedLength, + }) + ).to.throw( + `Input Length ${ + 9 + invalidPurpose.length + nonce.length + } must be under ${INT32_MAX_LIMIT} bytes` + ) + + expect(() => + setTimeout(() => { + kdfCounterMode({ + digestAlgorithm, + ikm, + nonce, + purpose: Buffer.alloc( + INT32_MAX_LIMIT - (4 + 4 + 1 + nonce.length) - 1 + ), + expectedLength, + }) + }, 2000) + ).to.not.throw() + }) + + describe('Raw derive', () => { + const explicitInfo = Buffer.alloc(10) + + it('Precondition: expected length must be positive', () => { + let invalidExpectedLength = -1 + expect(() => + rawDerive(ikm, explicitInfo, invalidExpectedLength, digestAlgorithm) + ).to.throw(`Requested length ${invalidExpectedLength} must be positive`) + + invalidExpectedLength = 0 + expect(() => + rawDerive(ikm, explicitInfo, invalidExpectedLength, digestAlgorithm) + ).to.throw(`Requested length ${invalidExpectedLength} must be positive`) + + expect(() => + rawDerive(ikm, explicitInfo, 1, digestAlgorithm) + ).to.not.throw() + }) + + it('Precondition: length of explicit info + 4 bytes should be under the max 32-bit signed integer', () => { + const invalidExplicitInfo = Buffer.alloc(INT32_MAX_LIMIT - 4) + expect(() => + rawDerive(ikm, invalidExplicitInfo, expectedLength, digestAlgorithm) + ).to.throw( + `Explicit info length ${invalidExplicitInfo.length} must be under ${ + INT32_MAX_LIMIT - 4 + } bytes` + ) + + expect(() => + setTimeout(() => { + rawDerive( + ikm, + Buffer.alloc(INT32_MAX_LIMIT - 4 - 1), + expectedLength, + digestAlgorithm + ) + }, 2000) + ).to.not.throw() + }) + + it('Precondition: the digest algorithm should be sha256', () => { + const invalidDigestAlgorithm = 'sha512' as SupportedDigestAlgorithms + expect(() => + rawDerive(ikm, explicitInfo, expectedLength, invalidDigestAlgorithm) + ).to.throw(`Unsupported digest algorithm ${invalidDigestAlgorithm}`) + + expect(() => + rawDerive(ikm, explicitInfo, expectedLength, 'sha256') + ).to.not.throw() + }) + + it('Precondition: the expected length + digest hash length should be under the max 32-bit signed integer - 1', () => { + const macLengthBytes = createHash('sha256').digest().length + const invalidExpectedLength = INT32_MAX_LIMIT - 1 - macLengthBytes + expect(() => + rawDerive(ikm, explicitInfo, invalidExpectedLength, 'sha256') + ).to.throw( + `The combined requested and digest hash length ${ + invalidExpectedLength + macLengthBytes + } must be under ${INT32_MAX_LIMIT - 1} bytes` + ) + }) + }) + + describe('Increment', () => { + it('Precondition: buffer length must be 4 bytes', () => { + let x = Buffer.alloc(5) + expect(() => increment(x)).to.throw( + `Buffer length ${x.length} must be 4 bytes` + ) + + x = Buffer.alloc(4) + expect(() => increment(x)).to.not.throw() + }) + + it('Postcondition: incremented buffer length must be 4 bytes', () => { + const a = Buffer.from([0, 0, 0, 250]) + expect(increment(a).length).equals(4) + const b = Buffer.from([0, 0, 250, 255]) + expect(increment(b).length).equals(4) + const c = Buffer.from([0, 250, 255, 255]) + expect(increment(c).length).equals(4) + const d = Buffer.from([250, 255, 255, 255]) + expect(increment(d).length).equals(4) + }) + + it('4th byte is incremented', () => { + const x = Buffer.from([0, 0, 0, 254]) + expect(increment(x)).to.deep.equals(Buffer.from([0, 0, 0, 255])) + }) + + it('3rd byte is incremented', () => { + const x = Buffer.from([0, 0, 254, 255]) + expect(increment(x)).deep.equals(Buffer.from([0, 0, 255, 0])) + }) + + it('2nd byte is incremented', () => { + const x = Buffer.from([0, 254, 255, 255]) + expect(increment(x)).deep.equals(Buffer.from([0, 255, 0, 0])) + }) + + it('1st byte is incremented', () => { + const x = Buffer.from([254, 255, 255, 255]) + expect(increment(x)).deep.equals(Buffer.from([255, 0, 0, 0])) + }) + + it('Buffer is maxed out and cannot be incremented', () => { + const x = Buffer.from([255, 255, 255, 255]) + expect(() => increment(x)).to.throw() + }) + }) + + describe('Test vectors', () => { + describe('Raw derive', () => { + for (const rawTestVector of rawTestVectors) { + const { name, hash, ikm, info, L, okm } = rawTestVector + it(name, () => { + expect(rawDerive(ikm, info, L, hash)).to.deep.equals(okm) + }) + } + }) + + for (const testVector of testVectors) { + const { name, hash, ikm, info, L, okm, purpose } = testVector + it(name, () => { + expect( + kdfCounterMode({ + digestAlgorithm: hash, + ikm, + nonce: info, + purpose, + expectedLength: L, + }) + ).to.deep.equals(okm) + }) + } + }) +}) diff --git a/modules/kdf-ctr-mode-node/test/testvectors.ts b/modules/kdf-ctr-mode-node/test/testvectors.ts new file mode 100644 index 000000000..950f25487 --- /dev/null +++ b/modules/kdf-ctr-mode-node/test/testvectors.ts @@ -0,0 +1,218 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// based on https://github.com/aws/aws-cryptographic-material-providers-library/blob/main/AwsCryptographyPrimitives/test/TestKDF_TestVectors.dfy + +import { + SupportedDigestAlgorithms, + SupportedDerivedKeyLengths, +} from '../src/kdfctr' + +interface InternalTestVector { + name: string + hash: SupportedDigestAlgorithms + ikm: Buffer + info: Buffer + L: number + okm: Buffer +} + +interface TestVector extends InternalTestVector { + purpose: Buffer + L: SupportedDerivedKeyLengths +} + +const PURPOSE = Buffer.from('aws-kms-hierarchy', 'utf-8') + +const b1: InternalTestVector = { + name: 'B.1 Test Case 1', + hash: 'sha256', + ikm: Buffer.from([ + 226, 4, 214, 212, 102, 170, 213, 7, 255, 175, 109, 109, 171, 10, 91, 38, 21, + 44, 158, 33, 231, 100, 55, 4, 100, 227, 96, 200, 251, 199, 101, 198, + ]), + info: Buffer.from([ + 123, 3, 185, 141, 159, 148, 184, 153, 229, 145, 243, 239, 38, 75, 113, 177, + 147, 251, 167, 4, 60, 126, 149, 60, 222, 35, 188, 83, 132, 188, 26, 98, 147, + 88, 1, 21, 250, 227, 73, 95, 216, 69, 218, 219, 208, 43, 214, 69, 92, 244, + 141, 15, 98, 179, 62, 98, 54, 74, 58, 128, + ]), + L: 32, + okm: Buffer.from([ + 119, 13, 250, 182, 166, 164, 164, 190, 224, 37, 127, 243, 53, 33, 63, 120, + 216, 40, 123, 79, 213, 55, 213, 193, 255, 250, 149, 105, 16, 231, 199, 121, + ]), +} + +const b2: InternalTestVector = { + name: 'B.2 Test Case 2', + hash: 'sha256', + ikm: Buffer.from([ + 174, 238, 202, 96, 246, 137, 164, 65, 177, 59, 12, 188, 212, 65, 216, 45, + 240, 207, 135, 218, 194, 54, 41, 13, 236, 232, 147, 29, 248, 215, 3, 23, + ]), + info: Buffer.from([ + 88, 142, 192, 65, 229, 115, 59, 112, 49, 33, 44, 85, 56, 239, 228, 246, 170, + 250, 76, 218, 139, 146, 93, 38, 31, 90, 38, 136, 240, 7, 179, 172, 36, 14, + 225, 41, 145, 231, 123, 140, 184, 83, 134, 120, 97, 89, 102, 22, 74, 129, + 135, 43, 209, 207, 203, 251, 57, 164, 244, 80, + ]), + L: 32, + okm: Buffer.from([ + 62, 129, 214, 17, 60, 238, 60, 82, 158, 206, 223, 248, 154, 105, 153, 206, + 37, 182, 24, 193, 94, 225, 209, 157, 69, 203, 55, 106, 28, 142, 35, 116, + ]), +} + +const b3: InternalTestVector = { + name: 'B.3 Test Case 3', + hash: 'sha256', + ikm: Buffer.from([ + 149, 200, 247, 110, 17, 54, 126, 181, 85, 38, 162, 179, 147, 174, 144, 101, + 131, 209, 203, 221, 71, 150, 33, 70, 245, 6, 204, 124, 172, 18, 244, 100, + ]), + info: Buffer.from([ + 202, 214, 14, 144, 75, 158, 156, 139, 254, 180, 168, 26, 127, 103, 211, 189, + 220, 192, 94, 100, 37, 88, 112, 64, 55, 112, 243, 83, 58, 230, 221, 99, 76, + 234, 165, 108, 83, 230, 136, 189, 19, 122, 230, 1, 137, 53, 243, 75, 159, + 176, 132, 234, 72, 228, 198, 136, 246, 187, 179, 136, + ]), + L: 32, + okm: Buffer.from([ + 202, 250, 92, 160, 63, 95, 190, 42, 36, 32, 4, 171, 203, 211, 222, 16, 89, + 199, 64, 123, 30, 229, 121, 37, 81, 36, 175, 24, 155, 224, 181, 86, + ]), +} + +const b4: InternalTestVector = { + name: 'B.4 Test Case 4', + hash: 'sha256', + ikm: Buffer.from([ + 77, 5, 57, 31, 214, 251, 30, 41, 46, 120, 171, 150, 25, 177, 183, 42, 125, + 99, 238, 89, 215, 67, 93, 215, 24, 151, 185, 255, 126, 231, 174, 112, + ]), + info: Buffer.from([ + 240, 120, 230, 249, 183, 248, 45, 100, 85, 79, 166, 182, 4, 200, 8, 241, + 155, 31, 106, 214, 114, 125, 183, 170, 111, 28, 134, 105, 78, 16, 75, 82, + 86, 200, 180, 3, 153, 25, 100, 100, 129, 215, 234, 36, 82, 199, 44, 23, 163, + 232, 215, 211, 145, 98, 133, 70, 10, 165, 235, 129, + ]), + L: 32, + okm: Buffer.from([ + 107, 22, 232, 245, 59, 131, 26, 165, 232, 107, 249, 122, 92, 79, 163, 125, + 8, 155, 193, 114, 218, 90, 30, 127, 102, 45, 212, 165, 149, 51, 154, 183, + ]), +} + +const b5: InternalTestVector = { + name: 'B.5 Test Case 5', + hash: 'sha256', + ikm: Buffer.from([ + 15, 104, 168, 47, 241, 103, 22, 52, 204, 145, 54, 197, 100, 169, 224, 42, + 118, 118, 33, 221, 116, 161, 191, 92, 36, 18, 155, 128, 130, 20, 183, 82, + ]), + info: Buffer.from([ + 100, 133, 153, 128, 156, 44, 78, 124, 106, 94, 108, 68, 159, 0, 49, 235, + 245, 92, 54, 97, 168, 149, 180, 77, 176, 87, 46, 232, 128, 131, 177, 244, + 177, 38, 2, 170, 85, 252, 29, 241, 80, 166, 90, 109, 110, 237, 160, 170, + 121, 164, 52, 161, 3, 155, 145, 181, 165, 143, 199, 241, + ]), + L: 32, + okm: Buffer.from([ + 226, 151, 100, 15, 119, 104, 72, 93, 74, 110, 124, 254, 36, 95, 139, 250, + 132, 112, 13, 153, 118, 38, 146, 234, 26, 66, 92, 204, 2, 117, 232, 245, + ]), +} + +const c1: TestVector = { + name: 'C.1 Test Case 1', + hash: 'sha256', + ikm: Buffer.from([ + 125, 201, 189, 252, 37, 52, 4, 124, 254, 99, 233, 235, 41, 123, 119, 82, 81, + 73, 237, 125, 74, 252, 233, 198, 68, 15, 53, 14, 97, 239, 62, 208, + ]), + info: Buffer.from([ + 119, 218, 233, 62, 104, 155, 88, 29, 62, 6, 235, 1, 200, 211, 186, 2, + ]), + purpose: PURPOSE, + L: 32, + okm: Buffer.from([ + 188, 232, 152, 114, 85, 137, 174, 192, 143, 152, 52, 179, 184, 15, 220, 63, + 241, 115, 144, 126, 85, 116, 231, 41, 253, 206, 18, 124, 247, 109, 183, 204, + ]), +} + +const c2: TestVector = { + name: 'C.2 Test Case 2', + hash: 'sha256', + ikm: Buffer.from([ + 80, 22, 113, 23, 118, 68, 10, 32, 75, 169, 199, 192, 255, 220, 214, 60, 182, + 1, 126, 147, 171, 233, 110, 177, 35, 145, 217, 129, 30, 9, 80, 159, + ]), + info: Buffer.from([ + 210, 241, 192, 158, 103, 66, 27, 35, 143, 66, 168, 189, 82, 171, 211, 252, + ]), + purpose: PURPOSE, + L: 32, + okm: Buffer.from([ + 54, 206, 174, 72, 237, 133, 85, 156, 93, 53, 120, 152, 118, 82, 89, 33, 114, + 98, 204, 236, 138, 57, 162, 118, 85, 92, 199, 232, 240, 252, 92, 97, + ]), +} + +const c3: TestVector = { + name: 'C.3 Test Case 3', + hash: 'sha256', + ikm: Buffer.from([ + 57, 90, 16, 46, 83, 54, 189, 241, 27, 242, 237, 236, 246, 66, 54, 226, 74, + 112, 79, 156, 208, 13, 148, 71, 117, 211, 139, 57, 73, 69, 122, 236, + ]), + info: Buffer.from([ + 51, 15, 183, 124, 82, 229, 249, 86, 117, 148, 237, 162, 27, 243, 173, 108, + ]), + purpose: PURPOSE, + L: 32, + okm: Buffer.from([ + 22, 55, 236, 141, 159, 163, 250, 236, 86, 47, 225, 103, 156, 225, 228, 146, + 166, 45, 244, 39, 136, 163, 205, 200, 116, 193, 20, 147, 112, 254, 210, 194, + ]), +} + +const c4: TestVector = { + name: 'C.4 Test Case 4', + hash: 'sha256', + ikm: Buffer.from([ + 152, 192, 25, 223, 239, 154, 175, 67, 237, 250, 184, 146, 228, 243, 227, 1, + 128, 247, 228, 152, 142, 131, 149, 41, 60, 70, 244, 58, 166, 234, 86, 189, + ]), + info: Buffer.from([ + 243, 160, 102, 127, 219, 137, 115, 38, 187, 216, 48, 80, 151, 168, 148, 71, + ]), + purpose: PURPOSE, + L: 32, + okm: Buffer.from([ + 191, 112, 86, 234, 220, 233, 122, 154, 100, 188, 230, 238, 239, 155, 54, 32, + 97, 35, 51, 160, 121, 235, 42, 64, 145, 105, 15, 153, 162, 89, 9, 156, + ]), +} + +const c5: TestVector = { + name: 'C.5 Test Case 5', + hash: 'sha256', + ikm: Buffer.from([ + 166, 236, 116, 51, 140, 189, 192, 175, 42, 154, 51, 26, 208, 149, 76, 159, + 174, 162, 207, 4, 108, 232, 196, 240, 12, 57, 228, 155, 97, 75, 42, 66, + ]), + info: Buffer.from([ + 236, 169, 233, 45, 43, 25, 122, 243, 152, 191, 154, 55, 45, 134, 159, 220, + ]), + purpose: PURPOSE, + L: 32, + okm: Buffer.from([ + 156, 11, 20, 251, 100, 227, 163, 161, 30, 45, 242, 2, 248, 246, 44, 11, 88, + 132, 189, 175, 95, 96, 61, 44, 98, 160, 212, 136, 140, 222, 57, 151, + ]), +} + +export const rawTestVectors = [b1, b2, b3, b4, b5] +export const testVectors = [c1, c2, c3, c4, c5] diff --git a/modules/kdf-ctr-mode-node/tsconfig.json b/modules/kdf-ctr-mode-node/tsconfig.json new file mode 100644 index 000000000..934011a31 --- /dev/null +++ b/modules/kdf-ctr-mode-node/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.settings.json", + "compilerOptions": { + "outDir": "build/main", + "rootDir": "./" + }, + "include": ["src/**/*.ts", "test/**/*.ts"], + "exclude": ["node_modules/**"], + "references": [{ "path": "../material-management" }] +} diff --git a/modules/kdf-ctr-mode-node/tsconfig.module.json b/modules/kdf-ctr-mode-node/tsconfig.module.json new file mode 100644 index 000000000..50bf04db4 --- /dev/null +++ b/modules/kdf-ctr-mode-node/tsconfig.module.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig", + "compilerOptions": { + "target": "esnext", + "outDir": "build/module", + "module": "esnext", + "allowSyntheticDefaultImports": true + }, + "exclude": [ + "node_modules/**" + ] +} \ No newline at end of file diff --git a/modules/kms-keyring-node/package.json b/modules/kms-keyring-node/package.json index 70d7e8fd3..1cc62bac7 100644 --- a/modules/kms-keyring-node/package.json +++ b/modules/kms-keyring-node/package.json @@ -19,9 +19,15 @@ }, "license": "Apache-2.0", "dependencies": { + "@aws-crypto/branch-keystore-node": "file:../branch-keystore-node", + "@aws-crypto/cache-material": "file:../cache-material", + "@aws-crypto/kdf-ctr-mode-node": "file:../kdf-ctr-mode-node", "@aws-crypto/kms-keyring": "file:../kms-keyring", "@aws-crypto/material-management-node": "file:../material-management-node", + "@aws-crypto/serialize": "file:../serialize", + "@aws-sdk/client-dynamodb": "^3.621.0", "@aws-sdk/client-kms": "^3.362.0", + "sinon": "^18.0.0", "tslib": "^2.2.0" }, "sideEffects": false, diff --git a/modules/kms-keyring-node/src/constants.ts b/modules/kms-keyring-node/src/constants.ts new file mode 100644 index 000000000..78b20d864 --- /dev/null +++ b/modules/kms-keyring-node/src/constants.ts @@ -0,0 +1,29 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { KeyringTraceFlag } from '@aws-crypto/material-management' + +export const ACTIVE_AS_BYTES = Buffer.from('ACTIVE', 'utf-8') +export const CACHE_ENTRY_ID_DIGEST_ALGORITHM = 'sha384' +export const KDF_DIGEST_ALGORITHM_SHA_256 = 'sha256' +export const ENCRYPT_FLAGS = + KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY | + KeyringTraceFlag.WRAPPING_KEY_SIGNED_ENC_CTX +export const DECRYPT_FLAGS = + KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY | + KeyringTraceFlag.WRAPPING_KEY_VERIFIED_ENC_CTX +export const PROVIDER_ID_HIERARCHY = 'aws-kms-hierarchy' +export const PROVIDER_ID_HIERARCHY_AS_BYTES = Buffer.from( + PROVIDER_ID_HIERARCHY, + 'utf-8' +) +export const DERIVED_BRANCH_KEY_LENGTH = 32 +// export const CACHE_ENTRY_ID_LENGTH = 32 +export const KEY_DERIVATION_LABEL = Buffer.from(PROVIDER_ID_HIERARCHY, 'utf-8') +export const CIPHERTEXT_STRUCTURE = { + saltLength: 16, + ivLength: 12, + branchKeyVersionCompressedLength: 16, + // Encrypted Key is of variable length + authTagLength: 16, +} diff --git a/modules/kms-keyring-node/src/index.ts b/modules/kms-keyring-node/src/index.ts index d6fcd98dc..2b9617dd2 100644 --- a/modules/kms-keyring-node/src/index.ts +++ b/modules/kms-keyring-node/src/index.ts @@ -6,3 +6,4 @@ 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' +export * from './kms_hkeyring_node' diff --git a/modules/kms-keyring-node/src/kms_hkeyring_node.ts b/modules/kms-keyring-node/src/kms_hkeyring_node.ts new file mode 100644 index 000000000..601a8e1bc --- /dev/null +++ b/modules/kms-keyring-node/src/kms_hkeyring_node.ts @@ -0,0 +1,487 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +/** + * This class is the KMS H-keyring. This class is within the kms-keyring-node + * module because it is a KMS keyring variation. However, the KDF used in this + * keyring's operations will only work in Node.js runtimes and not browser JS. + * Thus, this H-keyring implementation is only Node compatible, and thus, + * resides in a node module, not a browser module + */ + +import { + EncryptedDataKey, + immutableClass, + KeyringNode, + needs, + NodeAlgorithmSuite, + NodeDecryptionMaterial, + NodeEncryptionMaterial, + readOnlyProperty, + Catchable, + DecryptionMaterial, + isDecryptionMaterial, +} from '@aws-crypto/material-management' +import { + BranchKeyMaterialEntry, + CryptographicMaterialsCache, + getLocalCryptographicMaterialsCache, +} from '@aws-crypto/cache-material' +import { + destructureCiphertext, + getBranchKeyId, + getBranchKeyMaterials, + getCacheEntryId, + getPlaintextDataKey, + wrapPlaintextDataKey, + unwrapEncryptedDataKey, + filterEdk, + modifyEncryptionMaterial, + modifyDencryptionMaterial, + decompressBytesToUuidv4, + stringToUtf8Bytes, +} from './kms_hkeyring_node_helpers' +import { + BranchKeyStoreNode, + isIBranchKeyStoreNode, +} from '@aws-crypto/branch-keystore-node' +import { + BranchKeyIdSupplier, + isBranchKeyIdSupplier, +} from '@aws-crypto/kms-keyring' +import { randomBytes } from 'crypto' + +export interface KmsHierarchicalKeyRingNodeInput { + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#initialization + //= type=implication + //# - MUST provide either a Branch Key Identifier or a [Branch Key Supplier](#branch-key-supplier) + branchKeyId?: string + branchKeyIdSupplier?: BranchKeyIdSupplier + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#initialization + //= type=implication + //# - MUST provide a [Keystore](../branch-key-store.md) + keyStore: BranchKeyStoreNode + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#initialization + //= type=implication + //# - MUST provide a [cache limit TTL](#cache-limit-ttl) + cacheLimitTtl: number + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#initialization + //= type=exception + //# - MAY provide a [Cache Type](#cache-type) + cache?: CryptographicMaterialsCache + maxCacheSize?: number + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#initialization + //= type=implication + //# - MAY provide a [Partition ID](#partition-id) + partitionId?: string +} + +export interface IKmsHierarchicalKeyRingNode extends KeyringNode { + branchKeyId?: string + branchKeyIdSupplier?: Readonly + keyStore: Readonly + cacheLimitTtl: number + _onEncrypt(material: NodeEncryptionMaterial): Promise + _onDecrypt( + material: NodeDecryptionMaterial, + encryptedDataKeys: EncryptedDataKey[] + ): Promise + cacheEntryHasExceededLimits(entry: BranchKeyMaterialEntry): boolean +} + +export class KmsHierarchicalKeyRingNode + extends KeyringNode + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#interface + //= type=implication + //# MUST implement the [AWS Encryption SDK Keyring interface](../keyring-interface.md#interface) + implements IKmsHierarchicalKeyRingNode +{ + public declare branchKeyId?: string + public declare branchKeyIdSupplier?: Readonly + public declare keyStore: Readonly + public declare _logicalKeyStoreName: Buffer + public declare cacheLimitTtl: number + public declare maxCacheSize?: number + public declare _cmc: CryptographicMaterialsCache + declare readonly _partition: Buffer + + constructor({ + branchKeyId, + branchKeyIdSupplier, + keyStore, + cacheLimitTtl, + cache, + maxCacheSize, + partitionId, + }: KmsHierarchicalKeyRingNodeInput) { + super() + + needs( + !partitionId || typeof partitionId === 'string', + 'Partition id must be a string.' + ) + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#partition-id + //= type=implication + //# The Partition ID MUST NOT be changed after initialization. + readOnlyProperty( + this, + '_partition', + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#partition-id-1 + //# It can either be a String provided by the user, which MUST be interpreted as the bytes of + //# UTF-8 Encoding of the String, or a v4 UUID, which SHOULD be interpreted as the 16 byte representation of the UUID. + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#partition-id-1 + //# The constructor of the Hierarchical Keyring MUST record these bytes at construction time. + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#partition-id + //# If provided, it MUST be interpreted as UTF8 bytes. + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#partition-id + //= type=exception + //# If the PartitionId is NOT provided by the user, it MUST be set to the 16 byte representation of a v4 UUID. + partitionId ? stringToUtf8Bytes(partitionId) : randomBytes(64) + ) + + /* Precondition: The branch key id must be a string */ + if (branchKeyId) { + needs( + typeof branchKeyId === 'string', + 'The branch key id must be a string' + ) + } else { + branchKeyId = undefined + } + + /* Precondition: The branch key id supplier must be a BranchKeyIdSupplier */ + if (branchKeyIdSupplier) { + needs( + isBranchKeyIdSupplier(branchKeyIdSupplier), + 'The branch key id supplier must be a BranchKeyIdSupplier' + ) + } else { + branchKeyIdSupplier = undefined + } + + /* Precondition: The keystore must be a BranchKeyStore */ + needs( + isIBranchKeyStoreNode(keyStore), + 'The keystore must be a BranchKeyStore' + ) + + readOnlyProperty( + this, + '_logicalKeyStoreName', + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#logical-key-store-name + //# Logical Key Store Name MUST be converted to UTF8 Bytes to be used in + //# the cache identifiers. + stringToUtf8Bytes(keyStore.getKeyStoreInfo().logicalKeyStoreName) + ) + + /* Precondition: The cache limit TTL must be a number */ + needs( + typeof cacheLimitTtl === 'number', + 'The cache limit TTL must be a number' + ) + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#cache-limit-ttl + //# The maximum amount of time in seconds that an entry within the cache may be used before it MUST be evicted. + //# The client MUST set a time-to-live (TTL) for [branch key materials](../structures.md#branch-key-materials) in the underlying cache. + //# This value MUST be greater than zero. + /* Precondition: Cache limit TTL must be non-negative and less than or equal to (Number.MAX_SAFE_INTEGER / 1000) seconds */ + // In the MPL, TTL can be a non-negative signed 64-bit integer. + // However, JavaScript numbers cannot safely represent integers beyond + // Number.MAX_SAFE_INTEGER. Thus, we will cap TTL in seconds such that TTL + // in ms is <= Number.MAX_SAFE_INTEGER. TTL could be a BigInt type but this + // would require casting back to a number in order to configure the CMC, + // which leads to a lossy conversion + needs( + 0 <= cacheLimitTtl && cacheLimitTtl * 1000 <= Number.MAX_SAFE_INTEGER, + 'Cache limit TTL must be non-negative and less than or equal to (Number.MAX_SAFE_INTEGER / 1000) seconds' + ) + + /* Precondition: Must provide a branch key identifier or supplier */ + needs( + branchKeyId || branchKeyIdSupplier, + 'Must provide a branch key identifier or supplier' + ) + + readOnlyProperty(this, 'keyStore', Object.freeze(keyStore)) + /* Postcondition: The keystore object is frozen */ + + // convert seconds to milliseconds + readOnlyProperty(this, 'cacheLimitTtl', cacheLimitTtl * 1000) + + readOnlyProperty(this, 'branchKeyId', branchKeyId) + + readOnlyProperty( + this, + 'branchKeyIdSupplier', + branchKeyIdSupplier + ? Object.freeze(branchKeyIdSupplier) + : branchKeyIdSupplier + ) + /* Postcondition: Provided branch key supplier must be frozen */ + + if (cache) { + needs(!maxCacheSize, 'Max cache size not supported when passing a cache.') + } else { + /* Precondition: The max cache size must be a number */ + needs( + // Order is important, 0 is a number but also false. + typeof maxCacheSize === 'number' || !maxCacheSize, + 'The max cache size must be a number' + ) + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#initialization + //# If no max cache size is provided, the cryptographic materials cache MUST be configured to a + //# max cache size of 1000. + maxCacheSize = maxCacheSize === 0 || maxCacheSize ? maxCacheSize : 1000 + /* Precondition: Max cache size must be non-negative and less than or equal Number.MAX_SAFE_INTEGER */ + needs( + 0 <= maxCacheSize && maxCacheSize <= Number.MAX_SAFE_INTEGER, + 'Max cache size must be non-negative and less than or equal Number.MAX_SAFE_INTEGER' + ) + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#initialization + //# On initialization the Hierarchical Keyring MUST initialize a [cryptographic-materials-cache](../local-cryptographic-materials-cache.md) with the configured cache limit TTL and the max cache size. + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#initialization + //# If the Hierarchical Keyring does NOT get a `Shared` cache on initialization, + //# it MUST initialize a [cryptographic-materials-cache](../local-cryptographic-materials-cache.md) + //# with the user provided cache limit TTL and the entry capacity. + cache = getLocalCryptographicMaterialsCache(maxCacheSize) + } + readOnlyProperty(this, 'maxCacheSize', maxCacheSize) + readOnlyProperty(this, '_cmc', cache) + + Object.freeze(this) + /* Postcondition: The HKR object must be frozen */ + } + + async _onEncrypt( + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#onencrypt + //= type=implication + //# OnEncrypt MUST take [encryption materials](../structures.md#encryption-materials) as input. + encryptionMaterial: NodeEncryptionMaterial + ): Promise { + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#onencrypt + //# The `branchKeyId` used in this operation is either the configured branchKeyId, if supplied, or the result of the `branchKeySupplier`'s + //# `getBranchKeyId` operation, using the encryption material's encryption context as input. + const branchKeyId = getBranchKeyId(this, encryptionMaterial) + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#onencrypt + //# The hierarchical keyring MUST use the formulas specified in [Appendix A](#appendix-a-cache-entry-identifier-formulas) + //# to compute the [cache entry identifier](../cryptographic-materials-cache.md#cache-identifier). + const cacheEntryId = getCacheEntryId( + this._logicalKeyStoreName, + this._partition, + branchKeyId + ) + + const branchKeyMaterials = await getBranchKeyMaterials( + this, + this._cmc, + branchKeyId, + cacheEntryId + ) + + // get a pdk (generate it if not already set) + const pdk = getPlaintextDataKey(encryptionMaterial) + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#onencrypt + //# If the keyring is unable to wrap a plaintext data key, OnEncrypt MUST fail + //# and MUST NOT modify the [decryption materials](structures.md#decryption-materials). + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#onencrypt + //# - MUST wrap a data key with the branch key materials according to the [branch key wrapping](#branch-key-wrapping) section. + const edk = wrapPlaintextDataKey( + pdk, + branchKeyMaterials, + encryptionMaterial + ) + + // return the modified encryption material with the new edk and newly + // generated pdk (if applicable) + return modifyEncryptionMaterial(encryptionMaterial, pdk, edk, branchKeyId) + } + + async onDecrypt( + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#ondecrypt + //= type=implication + //# OnDecrypt MUST take [decryption materials](../structures.md#decryption-materials) and a list of [encrypted data keys](../structures.md#encrypted-data-keys) as input. + material: NodeDecryptionMaterial, + encryptedDataKeys: EncryptedDataKey[] + ): Promise> { + needs(isDecryptionMaterial(material), 'Unsupported material type.') + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#ondecrypt + //# If the decryption materials already contain a `PlainTextDataKey`, OnDecrypt MUST fail. + /* Precondition: If the decryption materials already contain a PlainTextDataKey, OnDecrypt MUST fail */ + needs( + !material.hasUnencryptedDataKey, + 'Decryption materials already contain a plaintext data key' + ) + + needs( + encryptedDataKeys.every((edk) => edk instanceof EncryptedDataKey), + 'Unsupported EncryptedDataKey type' + ) + + const _material = await this._onDecrypt(material, encryptedDataKeys) + + needs( + material === _material, + 'New DecryptionMaterial instances can not be created.' + ) + + return material + } + + cacheEntryHasExceededLimits({ now }: BranchKeyMaterialEntry): boolean { + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#onencrypt + //# There MUST be a check (cacheEntryWithinLimits) to make sure that for the cache entry found, who's TTL has NOT expired, + //# `time.now() - cacheEntryCreationTime <= ttlSeconds` is true and + //# valid for TTL of the Hierarchical Keyring getting the cache entry. + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#ondecrypt + //# There MUST be a check (cacheEntryWithinLimits) to make sure that for the cache entry found, who's TTL has NOT expired, + //# `time.now() - cacheEntryCreationTime <= ttlSeconds` is true and + //# valid for TTL of the Hierarchical Keyring getting the cache entry. + + const age = Date.now() - now + return age > this.cacheLimitTtl + } + + async _onDecrypt( + decryptionMaterial: NodeDecryptionMaterial, + encryptedDataKeyObjs: EncryptedDataKey[] + ): Promise { + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#ondecrypt + //# The `branchKeyId` used in this operation is either the configured branchKeyId, if supplied, or the result of the `branchKeySupplier`'s + //# `getBranchKeyId` operation, using the decryption material's encryption context as input. + const branchKeyId = getBranchKeyId(this, decryptionMaterial) + + // filter out edk objects that don't match this keyring's configuration + const filteredEdkObjs = encryptedDataKeyObjs.filter((edkObj) => + filterEdk(branchKeyId, edkObj) + ) + + /* Precondition: There must be an encrypted data key that matches this keyring configuration */ + needs( + filteredEdkObjs.length > 0, + "There must be an encrypted data key that matches this keyring's configuration" + ) + + const errors: Catchable[] = [] + for (const { encryptedDataKey: ciphertext } of filteredEdkObjs) { + let udk: Uint8Array | undefined = undefined + try { + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#getitem-branch-keystore-ondecrypt + //# - Deserialize the UUID string representation of the `version` from the [encrypted data key](../structures.md#encrypted-data-key) [ciphertext](#ciphertext). + // get the branch key version (as compressed bytes) from the + // destructured ciphertext of the edk + const { branchKeyVersionAsBytesCompressed } = destructureCiphertext( + ciphertext, + decryptionMaterial.suite + ) + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#getitem-branch-keystore-ondecrypt + //# - The deserialized UUID string representation of the `version` + // uncompress the branch key version into regular utf8 bytes + const branchKeyVersionAsBytes = stringToUtf8Bytes( + decompressBytesToUuidv4(branchKeyVersionAsBytesCompressed) + ) + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#ondecrypt + //# The hierarchical keyring MUST use the OnDecrypt formula specified in [Appendix A](#decryption-materials) + //# in order to compute the [cache entry identifier](cryptographic-materials-cache.md#cache-identifier). + const cacheEntryId = getCacheEntryId( + this._logicalKeyStoreName, + this._partition, + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#getitem-branch-keystore-ondecrypt + //# OnDecrypt MUST calculate the following values: + branchKeyId, + branchKeyVersionAsBytes + ) + + // get the string representation of the branch key version + const branchKeyVersionAsString = decompressBytesToUuidv4( + branchKeyVersionAsBytesCompressed + ) + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#ondecrypt + //# To decrypt each encrypted data key in the filtered set, the hierarchical keyring MUST attempt + //# to find the corresponding [branch key materials](../structures.md#branch-key-materials) + //# from the underlying [cryptographic materials cache](../local-cryptographic-materials-cache.md). + const branchKeyMaterials = await getBranchKeyMaterials( + this, + this._cmc, + branchKeyId, + cacheEntryId, + branchKeyVersionAsString + ) + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#ondecrypt + //# - MUST unwrap the encrypted data key with the branch key materials according to the [branch key unwrapping](#branch-key-unwrapping) section. + udk = unwrapEncryptedDataKey( + ciphertext, + branchKeyMaterials, + decryptionMaterial + ) + } catch (e) { + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#ondecrypt + //# For each encrypted data key in the filtered set, one at a time, OnDecrypt MUST attempt to decrypt the encrypted data key. + //# If this attempt results in an error, then these errors MUST be collected. + errors.push({ errPlus: e }) + } + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#ondecrypt + //# If a decryption succeeds, this keyring MUST + //# add the resulting plaintext data key to the decryption materials and return the modified materials. + if (udk) { + return modifyDencryptionMaterial(decryptionMaterial, udk, branchKeyId) + } + } + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#ondecrypt + //# 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 + //# and MUST NOT modify the [decryption materials](structures.md#decryption-materials). + throw new Error( + errors.reduce( + (m, e, i) => `${m} Error #${i + 1} \n ${e.errPlus.stack} \n`, + 'Unable to decrypt data key' + ) + ) + } +} + +immutableClass(KmsHierarchicalKeyRingNode) + +// The JS version has not been released with a Storm Tracking CMC + +//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#initialization +//= type=exception +//# If the cache to initialize is a [Storm Tracking Cryptographic Materials Cache](../storm-tracking-cryptographic-materials-cache.md#overview) +//# then the [Grace Period](../storm-tracking-cryptographic-materials-cache.md#grace-period) MUST be less than the [cache limit TTL](#cache-limit-ttl). + +//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#initialization +//= type=exception +//# If no `cache` is provided, a `DefaultCache` MUST be configured with entry capacity of 1000. + +// These are not something we can enforce + +//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#logical-key-store-name +//= type=exception +//# > Note: Users MUST NEVER have two different physical Key Stores with the same Logical Key Store Name. + +//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#shared-cache-considerations +//= type=exception +//# Any keyring that has access to the `Shared` cache MAY be able to use materials +//# that it MAY or MAY NOT have direct access to. +//# +//# Users MUST make sure that all of Partition ID, Logical Key Store Name of the Key Store for the Hierarchical Keyring +//# and Branch Key ID are set to be the same for two Hierarchical Keyrings if and only they want the keyrings to share +//# cache entries. diff --git a/modules/kms-keyring-node/src/kms_hkeyring_node_helpers.ts b/modules/kms-keyring-node/src/kms_hkeyring_node_helpers.ts new file mode 100644 index 000000000..66ecdc28d --- /dev/null +++ b/modules/kms-keyring-node/src/kms_hkeyring_node_helpers.ts @@ -0,0 +1,615 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + EncryptedDataKey, + NodeEncryptionMaterial, + unwrapDataKey, + NodeAlgorithmSuite, + NodeDecryptionMaterial, + NodeBranchKeyMaterial, + KeyringTraceFlag, + needs, + EncryptionContext, +} from '@aws-crypto/material-management' +import { IKmsHierarchicalKeyRingNode } from './kms_hkeyring_node' +import { + createCipheriv, + createDecipheriv, + createHash, + randomBytes, +} from 'crypto' +// import { uInt32BE } from '@aws-crypto/serialize' +import { CryptographicMaterialsCache } from '@aws-crypto/cache-material' +import { kdfCounterMode } from '@aws-crypto/kdf-ctr-mode-node' +import { + // ACTIVE_AS_BYTES, + CACHE_ENTRY_ID_DIGEST_ALGORITHM, + // CACHE_ENTRY_ID_LENGTH, + CIPHERTEXT_STRUCTURE, + DECRYPT_FLAGS, + DERIVED_BRANCH_KEY_LENGTH, + ENCRYPT_FLAGS, + KDF_DIGEST_ALGORITHM_SHA_256, + KEY_DERIVATION_LABEL, + PROVIDER_ID_HIERARCHY, + PROVIDER_ID_HIERARCHY_AS_BYTES, +} from './constants' +import { BranchKeyIdSupplier } from '@aws-crypto/kms-keyring' +import { serializeFactory, uuidv4Factory } from '@aws-crypto/serialize' + +export const stringToUtf8Bytes = (input: string): Buffer => + Buffer.from(input, 'utf-8') +export const utf8BytesToString = (input: Buffer): string => + input.toString('utf-8') +const stringToHexBytes = (input: string): Uint8Array => + new Uint8Array(Buffer.from(input, 'hex')) +const hexBytesToString = (input: Uint8Array): string => + Buffer.from(input).toString('hex') +export const { uuidv4ToCompressedBytes, decompressBytesToUuidv4 } = + uuidv4Factory(stringToHexBytes, hexBytesToString) +export const { serializeEncryptionContext } = + serializeFactory(stringToUtf8Bytes) +// const stringToAsciiBytes = (input: string): Buffer => +// Buffer.from(input, 'ascii') + +export function getBranchKeyId( + { branchKeyId, branchKeyIdSupplier }: IKmsHierarchicalKeyRingNode, + { encryptionContext }: NodeEncryptionMaterial | NodeDecryptionMaterial +): string { + // use the branch key id attribute if it was set, otherwise use the branch key + // id supplier. The constructor ensures that either the branch key id or + // supplier is supplied to the keyring + return ( + branchKeyId || + (branchKeyIdSupplier as BranchKeyIdSupplier).getBranchKeyId( + encryptionContext + ) + ) +} + +const RESOURCE_ID = new Uint8Array([0x02]) +const NULL_BYTE = new Uint8Array([0x00]) +const DECRYPTION_SCOPE = new Uint8Array([0x02]) +const ENCRYPTION_SCOPE = new Uint8Array([0x01]) + +//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#appendix-a-cache-entry-identifier-formulas +//# When accessing the underlying cryptographic materials cache, +//# the hierarchical keyring MUST use the formulas specified in this appendix +//# in order to compute the [cache entry identifier](../cryptographic-materials-cache.md#cache-identifier). +export function getCacheEntryId( + logicalKeyStoreName: Buffer, + partitionId: Buffer, + branchKeyId: string, + versionAsBytes?: Buffer +): string { + // get branch key id as a byte array + const branchKeyIdAsBytes = stringToUtf8Bytes(branchKeyId) + + var entryInfo + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#resource-suffix + //# The aforementioned 4 definitions ([Resource Identifier](#resource-identifier), + //# [Scope Identifier](#scope-identifier), [Partition ID](#partition-id-1), and + //# [Resource Suffix](#resource-suffix)) MUST be appended together with the null byte, 0x00, + //# and the SHA384 of the result should be taken as the final cache identifier. + + if (!!versionAsBytes) { + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#decryption-materials + //# When the hierarchical keyring receives an OnDecrypt request, + //# it MUST calculate the cache entry identifier as the + //# SHA-384 hash of the following byte strings, in the order listed: + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#decryption-materials + //# All the above fields must be separated by a single NULL_BYTE `0x00`. + //# + //# | Field | Length (bytes) | Interpreted as | + //# | ---------------------- | -------------- | ------------------- | + //# | Resource ID | 1 | bytes | + //# | Null Byte | 1 | `0x00` | + //# | Scope ID | 1 | bytes | + //# | Null Byte | 1 | `0x00` | + //# | Partition ID | Variable | bytes | + //# | Null Byte | 1 | `0x00` | + //# | Logical Key Store Name | Variable | UTF-8 Encoded Bytes | + //# | Null Byte | 1 | `0x00` | + //# | Branch Key ID | Variable | UTF-8 Encoded Bytes | + //# | Null Byte | 1 | `0x00` | + //# | branch-key-version | 36 | UTF-8 Encoded Bytes | + + entryInfo = Buffer.concat([ + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#decryption-materials + //# - MUST be the Resource ID for the Hierarchical Keyring (0x02) + RESOURCE_ID, + NULL_BYTE, + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#decryption-materials + //# - MUST be the Scope ID for Decrypt (0x02) + DECRYPTION_SCOPE, + NULL_BYTE, + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#decryption-materials + //# - MUST be the Partition ID for the Hierarchical Keyring + partitionId, + NULL_BYTE, + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#decryption-materials + //# - MUST be the UTF8 encoded Logical Key Store Name of the keystore for the Hierarchical Keyring + logicalKeyStoreName, + NULL_BYTE, + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#decryption-materials + //# - MUST be the UTF8 encoded branch-key-id + branchKeyIdAsBytes, + NULL_BYTE, + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#decryption-materials + //# - MUST be the UTF8 encoded branch-key-version + versionAsBytes, + ]) + } else { + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#encryption-materials + //# When the hierarchical keyring receives an OnEncrypt request, + //# the cache entry identifier MUST be calculated as the + //# SHA-384 hash of the following byte strings, in the order listed: + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#encryption-materials + //# All the above fields must be separated by a single NULL_BYTE `0x00`. + //# + //# | Field | Length (bytes) | Interpreted as | + //# | ---------------------- | -------------- | ------------------- | + //# | Resource ID | 1 | bytes | + //# | Null Byte | 1 | `0x00` | + //# | Scope ID | 1 | bytes | + //# | Null Byte | 1 | `0x00` | + //# | Partition ID | Variable | bytes | + //# | Null Byte | 1 | `0x00` | + //# | Logical Key Store Name | Variable | UTF-8 Encoded Bytes | + //# | Null Byte | 1 | `0x00` | + //# | Branch Key ID | Variable | UTF-8 Encoded Bytes | + + entryInfo = Buffer.concat([ + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#encryption-materials + //# - MUST be the Resource ID for the Hierarchical Keyring (0x02) + RESOURCE_ID, + NULL_BYTE, + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#encryption-materials + //# - MUST be the Scope ID for Encrypt (0x01) + ENCRYPTION_SCOPE, + NULL_BYTE, + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#encryption-materials + //# - MUST be the Partition ID for the Hierarchical Keyring + partitionId, + NULL_BYTE, + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#encryption-materials + //# - MUST be the UTF8 encoded Logical Key Store Name of the keystore for the Hierarchical Keyring + logicalKeyStoreName, + NULL_BYTE, + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#encryption-materials + //# - MUST be the UTF8 encoded branch-key-id + branchKeyIdAsBytes, + ]) + } + + // const entryInfo = versionAsBytes + // ? Buffer.concat([ + // RESOURCE_ID, + // NULL_BYTE, + // DECRYPTION_SCOPE, + // NULL_BYTE, + // partitionId, + // NULL_BYTE, + // logicalKeyStoreName, + // NULL_BYTE, + // branchKeyIdAsBytes, + // NULL_BYTE, + // versionAsBytes, + // ]) + // : Buffer.concat([ + // RESOURCE_ID, + // NULL_BYTE, + // ENCRYPTION_SCOPE, + // NULL_BYTE, + // partitionId, + // NULL_BYTE, + // logicalKeyStoreName, + // NULL_BYTE, + // branchKeyIdAsBytes, + // ]) + + // encrypt the branch key id buffer with sha512 + return createHash(CACHE_ENTRY_ID_DIGEST_ALGORITHM) + .update(entryInfo) + .digest() + .toString() +} + +export async function getBranchKeyMaterials( + hKeyring: IKmsHierarchicalKeyRingNode, + cmc: CryptographicMaterialsCache, + branchKeyId: string, + cacheEntryId: string, + branchKeyVersion?: string +): Promise { + const { keyStore, cacheLimitTtl } = hKeyring + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#onencrypt + //# The hierarchical keyring MUST attempt to find [branch key materials](../structures.md#branch-key-materials) + //# from the underlying [cryptographic materials cache](../local-cryptographic-materials-cache.md). + const cacheEntry = cmc.getBranchKeyMaterial(cacheEntryId) + let branchKeyMaterials: NodeBranchKeyMaterial + // if the cache entry is false, branch key materials were not found + if (!cacheEntry || hKeyring.cacheEntryHasExceededLimits(cacheEntry)) { + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#onencrypt + //# If this is NOT true, then we MUST treat the cache entry as expired. + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#ondecrypt + //# If this is NOT true, then we MUST treat the cache entry as expired. + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#onencrypt + //# If a cache entry is not found or the cache entry is expired, the hierarchical keyring MUST attempt to obtain the branch key materials + //# by querying the backing branch keystore specified in the [retrieve OnEncrypt branch key materials](#query-branch-keystore-onencrypt) section. + //# If the keyring is not able to retrieve [branch key materials](../structures.md#branch-key-materials) + //# through the underlying cryptographic materials cache or + //# it no longer has access to them through the backing keystore, OnEncrypt MUST fail. + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#query-branch-keystore-onencrypt + //# Otherwise, OnEncrypt MUST fail. + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#getitem-branch-keystore-ondecrypt + //# Otherwise, OnDecrypt MUST fail. + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#query-branch-keystore-onencrypt + //# OnEncrypt MUST call the Keystore's [GetActiveBranchKey](../branch-key-store.md#getactivebranchkey) operation with the following inputs: + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#getitem-branch-keystore-ondecrypt + //# OnDecrypt MUST call the Keystore's [GetBranchKeyVersion](../branch-key-store.md#getbranchkeyversion) operation with the following inputs: + branchKeyMaterials = branchKeyVersion + ? await keyStore.getBranchKeyVersion(branchKeyId, branchKeyVersion) + : //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#query-branch-keystore-onencrypt + //# OnEncrypt MUST call the Keystore's [GetActiveBranchKey](../branch-key-store.md#getactivebranchkey) operation with the following inputs: + await keyStore.getActiveBranchKey(branchKeyId) + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#query-branch-keystore-onencrypt + //# If the Keystore's GetActiveBranchKey operation succeeds + //# the keyring MUST put the returned branch key materials in the cache using the + //# formula defined in [Appendix A](#appendix-a-cache-entry-identifier-formulas). + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#getitem-branch-keystore-ondecrypt + //# If the Keystore's GetBranchKeyVersion operation succeeds + //# the keyring MUST put the returned branch key materials in the cache using the + //# formula defined in [Appendix A](#appendix-a-cache-entry-identifier-formulas). + cmc.putBranchKeyMaterial(cacheEntryId, branchKeyMaterials, cacheLimitTtl) + } else { + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#ondecrypt + //# If a cache entry is found and the entry's TTL has not expired, the hierarchical keyring MUST use those branch key materials for key unwrapping. + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#onencrypt + //# If a cache entry is found and the entry's TTL has not expired, the hierarchical keyring MUST use those branch key materials for key wrapping. + branchKeyMaterials = cacheEntry.response + } + + return branchKeyMaterials +} + +//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#onencrypt +//# If the input [encryption materials](../structures.md#encryption-materials) do not contain a plaintext data key, +//# OnEncrypt MUST generate a random plaintext data key, according to the key length defined in the [algorithm suite](../algorithm-suites.md#encryption-key-length). +//# The process used to generate this random plaintext data key MUST use a secure source of randomness. +export function getPlaintextDataKey(material: NodeEncryptionMaterial) { + // get the pdk from the encryption material whether it is already set or we + // must randomly generate it + return new Uint8Array( + material.hasUnencryptedDataKey + ? unwrapDataKey(material.getUnencryptedDataKey()) + : randomBytes(material.suite.keyLengthBytes) + ) +} + +//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#branch-key-wrapping +//# To derive and encrypt a data key the keyring will follow the same key derivation and encryption as [AWS KMS](https://rwc.iacr.org/2018/Slides/Gueron.pdf). +//# The hierarchical keyring MUST: +//# 1. Generate a 16 byte random `salt` using a secure source of randomness +//# 1. Generate a 12 byte random `IV` using a secure source of randomness +//# 1. Use a [KDF in Counter Mode with a Pseudo Random Function with HMAC SHA 256](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-108r1.pdf) to derive a 32 byte `derivedBranchKey` data key with the following inputs: +//# - Use the `salt` as the salt. +//# - Use the branch key as the `key`. +//# - Use the UTF8 Encoded value "aws-kms-hierarchy" as the label. +//# 1. Encrypt a plaintext data key with the `derivedBranchKey` using `AES-GCM-256` with the following inputs: +//# - MUST use the `derivedBranchKey` as the AES-GCM cipher key. +//# - MUST use the plain text data key that will be wrapped by the `derivedBranchKey` as the AES-GCM message. +//# - MUST use the derived `IV` as the AES-GCM IV. +//# - MUST use an authentication tag byte of length 16. +//# - MUST use the serialized [AAD](#branch-key-wrapping-and-unwrapping-aad) as the AES-GCM AAD. +//# If OnEncrypt fails to do any of the above, OnEncrypt MUST fail. +export function wrapPlaintextDataKey( + pdk: Uint8Array, + branchKeyMaterials: NodeBranchKeyMaterial, + { encryptionContext }: NodeEncryptionMaterial +): Uint8Array { + // get what we need from branch key material to wrap the pdk + const branchKey = branchKeyMaterials.branchKey() + const { branchKeyIdentifier, branchKeyVersion: branchKeyVersionAsBytes } = + branchKeyMaterials + // compress the branch key version utf8 bytes + const branchKeyVersionAsBytesCompressed = Buffer.from( + uuidv4ToCompressedBytes(utf8BytesToString(branchKeyVersionAsBytes)) + ) + const branchKeyIdAsBytes = stringToUtf8Bytes(branchKeyIdentifier) + + // generate salt and IV + const salt = randomBytes(CIPHERTEXT_STRUCTURE.saltLength) + const iv = randomBytes(CIPHERTEXT_STRUCTURE.ivLength) + + // derive a key from the branch key + const derivedBranchKey = kdfCounterMode({ + digestAlgorithm: KDF_DIGEST_ALGORITHM_SHA_256, + ikm: branchKey, + nonce: salt, + purpose: KEY_DERIVATION_LABEL, + expectedLength: DERIVED_BRANCH_KEY_LENGTH, + }) + + // set up additional auth data + const wrappedAad = wrapAad( + branchKeyIdAsBytes, + branchKeyVersionAsBytesCompressed, + encryptionContext + ) + + // encrypt the pdk into an edk + const cipher = createCipheriv('aes-256-gcm', derivedBranchKey, iv).setAAD( + wrappedAad + ) + const edkCiphertext = Buffer.concat([cipher.update(pdk), cipher.final()]) + const authTag = cipher.getAuthTag() + + // wrap the edk into a ciphertext + const ciphertext = new Uint8Array( + Buffer.concat([ + salt, + iv, + branchKeyVersionAsBytesCompressed, + edkCiphertext, + authTag, + ]) + ) + return ciphertext +} + +//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#branch-key-wrapping-and-unwrapping-aad +//# To Encrypt and Decrypt the `wrappedDerivedBranchKey` the keyring MUST include the following values as part of the AAD for +//# the AES Encrypt/Decrypt calls. +//# To construct the AAD, the keyring MUST concatenate the following values +//# 1. "aws-kms-hierarchy" as UTF8 Bytes +//# 1. Value of `branch-key-id` as UTF8 Bytes +//# 1. [version](../structures.md#branch-key-version) as Bytes +//# 1. [encryption context](structures.md#encryption-context-1) from the input +//# [encryption materials](../structures.md#encryption-materials) according to the [encryption context serialization specification](../structures.md#serialization). +//# | Field | Length (bytes) | Interpreted as | +//# | ------------------- | -------------- | ---------------------------------------------------- | +//# | "aws-kms-hierarchy" | 17 | UTF-8 Encoded | +//# | branch-key-id | Variable | UTF-8 Encoded | +//# | version | 16 | Bytes | +//# | encryption context | Variable | [Encryption Context](../structures.md#serialization) | +//# If the keyring cannot serialize the encryption context, the operation MUST fail. +export function wrapAad( + branchKeyIdAsBytes: Buffer, + version: Buffer, + encryptionContext: EncryptionContext +) { + /* Precondition: Branch key version must be 16 bytes */ + needs(version.length === 16, 'Branch key version must be 16 bytes') + + /* The AAD section is uInt16BE(length) + AAD + * see: https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/message-format.html#header-aad + * However, we _only_ need the ADD. + * So, I just slice off the length. + */ + const aad = Buffer.from( + serializeEncryptionContext(encryptionContext).slice(2) + ) + + return Buffer.concat([ + PROVIDER_ID_HIERARCHY_AS_BYTES, + branchKeyIdAsBytes, + version, + aad, + ]) +} + +//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#onencrypt +//# Otherwise, OnEncrypt MUST append a new [encrypted data key](../structures.md#encrypted-data-key) +//# to the encrypted data key list in the [encryption materials](../structures.md#encryption-materials), constructed as follows: +//# - [ciphertext](../structures.md#ciphertext): MUST be serialized as the [hierarchical keyring ciphertext](#ciphertext) +//# - [key provider id](../structures.md#key-provider-id): MUST be UTF8 Encoded "aws-kms-hierarchy" +//# - [key provider info](../structures.md#key-provider-information): MUST be the UTF8 Encoded AWS DDB response `branch-key-id` +export function modifyEncryptionMaterial( + encryptionMaterial: NodeEncryptionMaterial, + pdk: Uint8Array, + edk: Uint8Array, + wrappingKeyName: string +): NodeEncryptionMaterial { + // if the pdk was already set in the encryption material, we should not reset + if (!encryptionMaterial.hasUnencryptedDataKey) { + encryptionMaterial.setUnencryptedDataKey(pdk, { + keyNamespace: PROVIDER_ID_HIERARCHY, + keyName: wrappingKeyName, + flags: KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY, + }) + } + + // add the edk (that we created during onEncrypt) to the encryption material + return encryptionMaterial.addEncryptedDataKey( + new EncryptedDataKey({ + providerId: PROVIDER_ID_HIERARCHY, + providerInfo: wrappingKeyName, + encryptedDataKey: edk, + }), + ENCRYPT_FLAGS + ) +} + +//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#ondecrypt +//# The set of encrypted data keys MUST first be filtered to match this keyring’s configuration. For the encrypted data key to match: +//# - Its provider ID MUST match the UTF8 Encoded value of “aws-kms-hierarchy”. +//# - Deserialize the key provider info, if deserialization fails the next EDK in the set MUST be attempted. +//# - The deserialized key provider info MUST be UTF8 Decoded and MUST match this keyring's configured `Branch Key Identifier`. +export function filterEdk( + branchKeyId: string, + { providerId, providerInfo }: EncryptedDataKey +): boolean { + // check if the edk matches the keyring's configuration according to provider + // id and info (the edk object should have been wrapped by the branch key + // configured in this keyring or decryption material's encryption context) + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#getitem-branch-keystore-ondecrypt + //# - Deserialize the UTF8-Decoded `branch-key-id` from the [key provider info](../structures.md#key-provider-information) of the [encrypted data key](../structures.md#encrypted-data-key) + //# and verify this is equal to the configured or supplied `branch-key-id`. + return providerId === PROVIDER_ID_HIERARCHY + ? branchKeyId === providerInfo + : false +} + +//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#ciphertext +//# The following table describes the fields that form the ciphertext for this keyring. +//# The bytes are appended in the order shown. +//# The Encryption Key is variable. +//# It will be whatever length is represented by the algorithm suite. +//# Because all the other values are constant, +//# this variability in the encryption key does not impact the format. +//# | Field | Length (bytes) | Interpreted as | +//# | ------------------ | -------------- | -------------- | +//# | Salt | 16 | bytes | +//# | IV | 12 | bytes | +//# | Version | 16 | bytes | +//# | Encrypted Key | Variable | bytes | +//# | Authentication Tag | 16 | bytes | +export function destructureCiphertext( + ciphertext: Uint8Array, + { keyLengthBytes }: NodeAlgorithmSuite +) { + // what we expect the length of the edk object's ciphertext to be. This + // depends on the byte key length specified by the algorithm suite + const expectedCiphertextLength = + CIPHERTEXT_STRUCTURE.saltLength + + CIPHERTEXT_STRUCTURE.ivLength + + CIPHERTEXT_STRUCTURE.branchKeyVersionCompressedLength + + keyLengthBytes + + CIPHERTEXT_STRUCTURE.authTagLength + /* Precondition: The edk ciphertext must have the correct length */ + needs( + ciphertext.length === expectedCiphertextLength, + `The encrypted data key ciphertext must be ${expectedCiphertextLength} bytes long` + ) + + let start = 0 + let end = 0 + + // extract the salt from the edk ciphertext + start = end + end += CIPHERTEXT_STRUCTURE.saltLength + const salt = Buffer.from(ciphertext.subarray(start, end)) + + // extract the IV from the edk ciphertext + start = end + end += CIPHERTEXT_STRUCTURE.ivLength + const iv = Buffer.from(ciphertext.subarray(start, end)) + + // extract the compressed branch key version from the edk ciphertext + start = end + end += CIPHERTEXT_STRUCTURE.branchKeyVersionCompressedLength + const branchKeyVersionAsBytesCompressed = Buffer.from( + ciphertext.subarray(start, end) + ) + + // extract the encrypted data key from the edk ciphertext + start = end + end += keyLengthBytes + const encryptedDataKey = Buffer.from(ciphertext.subarray(start, end)) + + // extract the auth tag from the edk ciphertext + start = end + end += CIPHERTEXT_STRUCTURE.authTagLength + const authTag = Buffer.from(ciphertext.subarray(start, end)) + + return { + salt, + iv, + branchKeyVersionAsBytesCompressed, + encryptedDataKey, + authTag, + } +} + +//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#branch-key-unwrapping +//# To decrypt an encrypted data key with a branch key, the hierarchical keyring MUST: +//# 1. Deserialize the 16 byte random `salt` from the [edk ciphertext](../structures.md#ciphertext). +//# 1. Deserialize the 12 byte random `IV` from the [edk ciphertext](../structures.md#ciphertext). +//# 1. Deserialize the 16 byte `version` from the [edk ciphertext](../structures.md#ciphertext). +//# 1. Deserialize the `encrypted key` from the [edk ciphertext](../structures.md#ciphertext). +//# 1. Deserialize the `authentication tag` from the [edk ciphertext](../structures.md#ciphertext). +//# 1. Use a [KDF in Counter Mode with a Pseudo Random Function with HMAC SHA 256](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-108r1.pdf) to derive +//# the 32 byte `derivedBranchKey` data key with the following inputs: +//# - Use the `salt` as the salt. +//# - Use the branch key as the `key`. +//# 1. Decrypt the encrypted data key with the `derivedBranchKey` using `AES-GCM-256` with the following inputs: +//# - It MUST use the `encrypted key` obtained from deserialization as the AES-GCM input ciphertext. +//# - It MUST use the `authentication tag` obtained from deserialization as the AES-GCM input authentication tag. +//# - It MUST use the `derivedBranchKey` as the AES-GCM cipher key. +//# - It MUST use the `IV` obtained from deserialization as the AES-GCM input IV. +//# - It MUST use the serialized [encryption context](#branch-key-wrapping-and-unwrapping-aad) as the AES-GCM AAD. +//# If OnDecrypt fails to do any of the above, OnDecrypt MUST fail. +export function unwrapEncryptedDataKey( + ciphertext: Uint8Array, + branchKeyMaterials: NodeBranchKeyMaterial, + { encryptionContext, suite }: NodeDecryptionMaterial +) { + // get what we need from the branch key materials to unwrap the edk + const branchKey = branchKeyMaterials.branchKey() + const { branchKeyIdentifier } = branchKeyMaterials + const branchKeyIdAsBytes = stringToUtf8Bytes(branchKeyIdentifier) + + // get the salt, iv, edk, and auth tag from the edk ciphertext + const { + salt, + iv, + encryptedDataKey, + authTag, + branchKeyVersionAsBytesCompressed, + } = destructureCiphertext(ciphertext, suite) + + // derive a key from the branch key + const derivedBranchKey = kdfCounterMode({ + digestAlgorithm: KDF_DIGEST_ALGORITHM_SHA_256, + ikm: branchKey, + nonce: salt, + purpose: KEY_DERIVATION_LABEL, + expectedLength: DERIVED_BRANCH_KEY_LENGTH, + }) + + // set up additional auth data + const wrappedAad = wrapAad( + branchKeyIdAsBytes, + branchKeyVersionAsBytesCompressed, + encryptionContext + ) + + // decipher the edk to get the udk/pdk + const decipher = createDecipheriv('aes-256-gcm', derivedBranchKey, iv) + .setAAD(wrappedAad) + .setAuthTag(authTag) + const udk = Buffer.concat([ + decipher.update(encryptedDataKey), + decipher.final(), + ]) + + return new Uint8Array(udk) +} + +export function modifyDencryptionMaterial( + decryptionMaterial: NodeDecryptionMaterial, + udk: Uint8Array, + wrappingKeyName: string +): NodeDecryptionMaterial { + // modify the decryption material by setting the plaintext data key + return decryptionMaterial.setUnencryptedDataKey(udk, { + keyNamespace: PROVIDER_ID_HIERARCHY, + keyName: wrappingKeyName, + flags: DECRYPT_FLAGS, + }) +} diff --git a/modules/kms-keyring-node/test/fixtures.ts b/modules/kms-keyring-node/test/fixtures.ts new file mode 100644 index 000000000..7c3473184 --- /dev/null +++ b/modules/kms-keyring-node/test/fixtures.ts @@ -0,0 +1,56 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + BranchKeyStoreNode, +} from '@aws-crypto/branch-keystore-node' +import { + AlgorithmSuiteIdentifier, + EncryptionContext, + NodeAlgorithmSuite, +} from '@aws-crypto/material-management' + +export const DDB_TABLE_NAME = 'KeyStoreDdbTable' +export const LOGICAL_KEYSTORE_NAME = DDB_TABLE_NAME +export const BRANCH_KEY_ID = '75789115-1deb-4fe3-a2ec-be9e885d1945' +export const BRANCH_KEY_ACTIVE_VERSION = 'fed7ad33-0774-4f97-aa5e-6c766fc8af9f' +export const BRANCH_KEY_ID_WITH_EC = '4bb57643-07c1-419e-92ad-0df0df149d7c' + +export const KEY_ARN = + 'arn:aws:kms:us-west-2:370957321024:key/9d989aa2-2f9c-438c-a745-cc57d3ad0126' + +export const TEST_ESDK_ALG_SUITE_ID = + AlgorithmSuiteIdentifier.ALG_AES256_GCM_IV12_TAG16 +export const TEST_ESDK_ALG_SUITE = new NodeAlgorithmSuite( + TEST_ESDK_ALG_SUITE_ID +) +export const TTL = 1 * 60000 * 10 +export const KEYSTORE = new BranchKeyStoreNode({ + storage: {ddbTableName: DDB_TABLE_NAME}, + logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, + kmsConfiguration: { identifier: KEY_ARN }, +}) + +// Constants for TestBranchKeySupplier +export const BRANCH_KEY = 'branchKey' +export const CASE_A = 'caseA' +export const CASE_B = 'caseB' +export const BRANCH_KEY_ID_A = BRANCH_KEY_ID +export const BRANCH_KEY_ID_B = BRANCH_KEY_ID_WITH_EC +export const DEFAULT_EC: EncryptionContext = { keyA: 'valA' } +export const EC_A: EncryptionContext = { [BRANCH_KEY]: CASE_A } +export const EC_B: EncryptionContext = { [BRANCH_KEY]: CASE_B } + +export const ALG_SUITE_IDS = [ + AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16, + AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16_HKDF_SHA256, + AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16_HKDF_SHA256_ECDSA_P256, + AlgorithmSuiteIdentifier.ALG_AES192_GCM_IV12_TAG16, + AlgorithmSuiteIdentifier.ALG_AES192_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + AlgorithmSuiteIdentifier.ALG_AES256_GCM_IV12_TAG16, + AlgorithmSuiteIdentifier.ALG_AES256_GCM_IV12_TAG16_HKDF_SHA256, + AlgorithmSuiteIdentifier.ALG_AES256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + AlgorithmSuiteIdentifier.ALG_AES256_GCM_IV12_TAG16_HKDF_SHA512_COMMIT_KEY, + AlgorithmSuiteIdentifier.ALG_AES256_GCM_IV12_TAG16_HKDF_SHA512_COMMIT_KEY_ECDSA_P384, +] +export const ALG_SUITES = ALG_SUITE_IDS.map((id) => new NodeAlgorithmSuite(id)) diff --git a/modules/kms-keyring-node/test/kms_hkeyring_node.constructor.test.ts b/modules/kms-keyring-node/test/kms_hkeyring_node.constructor.test.ts new file mode 100644 index 000000000..63c37aca0 --- /dev/null +++ b/modules/kms-keyring-node/test/kms_hkeyring_node.constructor.test.ts @@ -0,0 +1,292 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + BranchKeyStoreNode, +} from '@aws-crypto/branch-keystore-node' +import { + DDB_TABLE_NAME, + LOGICAL_KEYSTORE_NAME, + KEY_ARN, + BRANCH_KEY_ID, + TTL, +} from './fixtures' +import { KmsHierarchicalKeyRingNode } from '../src/kms_hkeyring_node' +import { expect } from 'chai' +import { BranchKeyIdSupplier } from '@aws-crypto/kms-keyring' +import { EncryptionContext } from '@aws-crypto/material-management' + +class DummyBranchKeyIdSupplier implements BranchKeyIdSupplier { + getBranchKeyId(encryptionContext: EncryptionContext): string { + return encryptionContext[''] ? encryptionContext[''] : '' + } +} + +const branchKeyId = BRANCH_KEY_ID +const cacheLimitTtl = TTL +const maxCacheSize = 1000 +const keyStore = new BranchKeyStoreNode({ + storage: {ddbTableName: DDB_TABLE_NAME}, + logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, + kmsConfiguration: { identifier: KEY_ARN }, +}) +const branchKeyIdSupplier = new DummyBranchKeyIdSupplier() +const hkr = new KmsHierarchicalKeyRingNode({ + branchKeyId, + branchKeyIdSupplier, + keyStore, + cacheLimitTtl, + maxCacheSize, +}) + +describe('KmsHierarchicalKeyRingNode: constructor', () => { + describe('Runtime type checks', () => { + const truthyValues = [1, 'string', true, {}, [], -1, 0.21] + const falseyValues = [false, 0, 0n, -0, 0x0, '', null, undefined, NaN] + const nonStringFilter = (v: any) => typeof v !== 'string' + const nonNumberFilter = (v: any) => typeof v !== 'number' + + it('Precondition: The branch key id must be a string', () => { + const nonStringFalseyValues = falseyValues.filter(nonStringFilter) + for (const branchKeyId of nonStringFalseyValues) { + const hkr = new KmsHierarchicalKeyRingNode({ + branchKeyId: branchKeyId as any, + branchKeyIdSupplier, + keyStore, + cacheLimitTtl, + }) + expect(hkr.branchKeyId).to.equal(undefined) + } + + const nonStringTruthyValues = truthyValues.filter(nonStringFilter) + for (const branchKeyId of nonStringTruthyValues) { + expect( + () => + new KmsHierarchicalKeyRingNode({ + branchKeyId: branchKeyId as any, + branchKeyIdSupplier, + keyStore, + cacheLimitTtl, + }) + ).to.throw('The branch key id must be a string') + } + }) + + it('Precondition: The branch key id supplier must be a BranchKeyIdSupplier', () => { + for (const branchKeyIdSupplier of falseyValues) { + const hkr = new KmsHierarchicalKeyRingNode({ + branchKeyIdSupplier: branchKeyIdSupplier as any, + branchKeyId, + keyStore, + cacheLimitTtl, + }) + expect(hkr.branchKeyIdSupplier).to.equal(undefined) + } + + for (const branchKeyIdSupplier of truthyValues) { + expect( + () => + new KmsHierarchicalKeyRingNode({ + branchKeyIdSupplier: branchKeyIdSupplier as any, + branchKeyId, + keyStore, + cacheLimitTtl, + }) + ).to.throw('The branch key id supplier must be a BranchKeyIdSupplier') + } + }) + + it('Precondition: The keystore must be a BranchKeyStore', () => { + for (const keyStore of [...truthyValues, ...falseyValues]) { + expect( + () => + new KmsHierarchicalKeyRingNode({ + branchKeyId, + keyStore: keyStore as any, + cacheLimitTtl, + }) + ).to.throw('The keystore must be a BranchKeyStore') + } + }) + + it('Precondition: The cache limit TTL must be a number', () => { + const ttls = [...falseyValues, ...truthyValues].filter(nonNumberFilter) + for (const cacheLimitTtl of ttls) { + expect( + () => + new KmsHierarchicalKeyRingNode({ + branchKeyId, + keyStore, + cacheLimitTtl: cacheLimitTtl as any, + }) + ).to.throw('The cache limit TTL must be a number') + } + }) + + it('Precondition: The max cache size must be a number', () => { + for (const maxCacheSize of falseyValues.filter(nonNumberFilter)) { + const hkr = new KmsHierarchicalKeyRingNode({ + branchKeyId, + keyStore, + cacheLimitTtl, + maxCacheSize: maxCacheSize as any, + }) + expect(hkr.maxCacheSize).to.equal(1000) + } + + for (const maxCacheSize of truthyValues.filter(nonNumberFilter)) { + expect( + () => + new KmsHierarchicalKeyRingNode({ + branchKeyId, + keyStore, + cacheLimitTtl, + maxCacheSize: maxCacheSize as any, + }) + ).to.throw('The max cache size must be a number') + } + + expect( + new KmsHierarchicalKeyRingNode({ + branchKeyId, + keyStore, + cacheLimitTtl, + maxCacheSize: 0, + }).maxCacheSize + ).to.equal(0) + }) + }) + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#cache-limit-ttl + //= type=test + //# The maximum amount of time in seconds that an entry within the cache may be used before it MUST be evicted. + //# The client MUST set a time-to-live (TTL) for [branch key materials](../structures.md#branch-key-materials) in the underlying cache. + //# This value MUST be greater than zero. + it('Precondition: Cache limit TTL must be non-negative and less than or equal to (Number.MAX_SAFE_INTEGER / 1000) seconds', () => { + expect( + new KmsHierarchicalKeyRingNode({ + branchKeyId, + keyStore, + cacheLimitTtl: 0, + }).cacheLimitTtl + ).to.equal(0) + + expect( + new KmsHierarchicalKeyRingNode({ + branchKeyId, + keyStore, + cacheLimitTtl: Number.MAX_SAFE_INTEGER / 1000, + }).cacheLimitTtl + ).to.equal((Number.MAX_SAFE_INTEGER / 1000) * 1000) + + expect( + () => + new KmsHierarchicalKeyRingNode({ + branchKeyId, + keyStore, + cacheLimitTtl: -1, + }) + ).to.throw( + 'Cache limit TTL must be non-negative and less than or equal to (Number.MAX_SAFE_INTEGER / 1000) seconds' + ) + + expect( + () => + new KmsHierarchicalKeyRingNode({ + branchKeyId, + keyStore, + cacheLimitTtl: Number.MAX_SAFE_INTEGER / 1000 + 1, + }) + ).to.throw( + 'Cache limit TTL must be non-negative and less than or equal to (Number.MAX_SAFE_INTEGER / 1000) seconds' + ) + }) + + it('Precondition: Must provide a branch key identifier or supplier', () => { + expect( + () => + new KmsHierarchicalKeyRingNode({ + keyStore, + cacheLimitTtl, + }) + ).to.throw('Must provide a branch key identifier or supplier') + }) + + it('Precondition: Max cache size must be non-negative and less than or equal Number.MAX_SAFE_INTEGER', () => { + expect( + new KmsHierarchicalKeyRingNode({ + branchKeyId, + keyStore, + cacheLimitTtl, + maxCacheSize: 0, + }).maxCacheSize + ).to.equal(0) + + expect( + new KmsHierarchicalKeyRingNode({ + branchKeyId, + keyStore, + cacheLimitTtl, + maxCacheSize: Number.MAX_SAFE_INTEGER, + }).maxCacheSize + ).to.equal(Number.MAX_SAFE_INTEGER) + + expect( + () => + new KmsHierarchicalKeyRingNode({ + branchKeyId, + keyStore, + cacheLimitTtl, + maxCacheSize: -1, + }) + ).to.throw( + 'Max cache size must be non-negative and less than or equal Number.MAX_SAFE_INTEGER' + ) + + expect( + () => + new KmsHierarchicalKeyRingNode({ + branchKeyId, + keyStore, + cacheLimitTtl, + maxCacheSize: Number.MAX_SAFE_INTEGER + 1, + }) + ).to.throw( + 'Max cache size must be non-negative and less than or equal Number.MAX_SAFE_INTEGER' + ) + }) + + it('Postcondition: The keystore object is frozen', () => { + expect(Object.isFrozen(hkr.keyStore)).equals(true) + }) + + it('Postcondition: Provided branch key supplier must be frozen', () => { + expect(Object.isFrozen(hkr.branchKeyIdSupplier)).equals(true) + }) + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#initialization + //= type=test + //# If no max cache size is provided, the cryptographic materials cache MUST be configured to a + //# max cache size of 1000. + it('Postcondition: The max cache size is initialized', () => { + expect( + new KmsHierarchicalKeyRingNode({ + branchKeyId, + keyStore, + cacheLimitTtl, + }).maxCacheSize + ).to.equal(maxCacheSize) + }) + + it('Postcondition: The HKR object must be frozen', () => { + expect(Object.isFrozen(hkr)).equals(true) + }) + + it('All attributes initialized correctly', () => { + expect(hkr.branchKeyId).to.equal(branchKeyId) + expect(hkr.branchKeyIdSupplier).to.equal(branchKeyIdSupplier) + expect(hkr.keyStore).to.equal(keyStore) + expect(hkr.cacheLimitTtl).to.equal(cacheLimitTtl * 1000) + expect(hkr.maxCacheSize).to.equal(maxCacheSize) + }) +}) diff --git a/modules/kms-keyring-node/test/kms_hkeyring_node.edk-order.test.ts b/modules/kms-keyring-node/test/kms_hkeyring_node.edk-order.test.ts new file mode 100644 index 000000000..75f0b6e82 --- /dev/null +++ b/modules/kms-keyring-node/test/kms_hkeyring_node.edk-order.test.ts @@ -0,0 +1,323 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { CIPHERTEXT_STRUCTURE, PROVIDER_ID_HIERARCHY } from '../src/constants' +import { + BRANCH_KEY_ACTIVE_VERSION, + BRANCH_KEY_ID, + BRANCH_KEY_ID_A, + BRANCH_KEY_ID_B, + DEFAULT_EC, + EC_A, + KEYSTORE, + TEST_ESDK_ALG_SUITE, + TTL, +} from './fixtures' +import { v4 } from 'uuid' +import { uuidv4ToCompressedBytes } from '../src/kms_hkeyring_node_helpers' +import { + EncryptedDataKey, + NodeBranchKeyMaterial, + NodeDecryptionMaterial, + NodeEncryptionMaterial, + unwrapDataKey, +} from '@aws-crypto/material-management' +import { KmsHierarchicalKeyRingNode } from '../src/kms_hkeyring_node' +import chai, { expect } from 'chai' +import chaiAsPromised from 'chai-as-promised' +import Sinon from 'sinon' +import { KMSClient } from '@aws-sdk/client-kms' +import { DynamoDBClient } from '@aws-sdk/client-dynamodb' +import { + BRANCH_KEY_ID_SUPPLIER, + deepCopyBranchKeyMaterial, + testOnDecrypt, + testOnDecryptError, + testOnEncrypt, +} from './kms_hkeyring_node.test' +import { BranchKeyStoreNode, KeyStoreInfoOutput } from '@aws-crypto/branch-keystore-node' +chai.use(chaiAsPromised) + +// an edk that can't even be destructured according to any alg suite +const malformedEdkCiphertext = new Uint8Array(Buffer.alloc(1)) + +// expected length of well-formed edk ciphertexts +const ciphertextLength = + CIPHERTEXT_STRUCTURE.saltLength + + CIPHERTEXT_STRUCTURE.ivLength + + CIPHERTEXT_STRUCTURE.branchKeyVersionCompressedLength + + TEST_ESDK_ALG_SUITE.keyLengthBytes + + CIPHERTEXT_STRUCTURE.authTagLength + +// an edk whose compressed branch key version cannot be decompressed as a uuidv4 +const badUuidEdkCiphertext = new Uint8Array(Buffer.alloc(ciphertextLength)) + +// an edk whose branch key version can be decompressed but is non-existent in +// the keystore +const nonExistentBranchKeyVersion = v4() +const nonExistentBranchKeyVersionEdkCiphertext = new Uint8Array( + badUuidEdkCiphertext +) +nonExistentBranchKeyVersionEdkCiphertext.set( + uuidv4ToCompressedBytes(nonExistentBranchKeyVersion), + CIPHERTEXT_STRUCTURE.saltLength + CIPHERTEXT_STRUCTURE.ivLength +) + +// an edk whose ciphertext cannot be unwrapped +const existingBranchKeyVersion = BRANCH_KEY_ACTIVE_VERSION +const nonUnwrappableEdkCiphertext = new Uint8Array(badUuidEdkCiphertext) +nonUnwrappableEdkCiphertext.set( + uuidv4ToCompressedBytes(existingBranchKeyVersion), + CIPHERTEXT_STRUCTURE.saltLength + CIPHERTEXT_STRUCTURE.ivLength +) + +const badCiphertexts = [ + malformedEdkCiphertext, + badUuidEdkCiphertext, + nonExistentBranchKeyVersionEdkCiphertext, + nonUnwrappableEdkCiphertext, +] + +const branchKeyId = BRANCH_KEY_ID +const branchKeyIdSupplier = BRANCH_KEY_ID_SUPPLIER +const originalKeyStore = KEYSTORE +const cacheLimitTtl = TTL + +// create bad edks that pass the filter but fail decryption for different +// reasons due to their bad ciphertexts +const badEdks = badCiphertexts.map( + (badCiphertext) => + new EncryptedDataKey({ + providerId: PROVIDER_ID_HIERARCHY, + providerInfo: branchKeyId, + encryptedDataKey: badCiphertext, + }) +) + +// before all tests run, get the active and versioned branch key materials +let activeBranchKeyMaterial: NodeBranchKeyMaterial +let activeVersion: string +let versionedBranchKeyMaterial: NodeBranchKeyMaterial +before(async function () { + activeBranchKeyMaterial = await originalKeyStore.getActiveBranchKey( + branchKeyId + ) + activeVersion = activeBranchKeyMaterial.branchKeyVersion.toString('utf-8') + + versionedBranchKeyMaterial = await originalKeyStore.getBranchKeyVersion( + branchKeyId, + activeVersion + ) +}) + +describe('KmsHierarchicalKeyRingNode: decrypt EDK order', () => { + let kmsSendSpy: any + let ddbSendSpy: any + let keyStore: Sinon.SinonStubbedInstance + + beforeEach(() => { + keyStore = Sinon.createStubInstance(BranchKeyStoreNode) + kmsSendSpy = Sinon.spy(KMSClient.prototype, 'send') + ddbSendSpy = Sinon.spy(DynamoDBClient.prototype, 'send') + + keyStore.getActiveBranchKey.callsFake(async function (id: string) { + if (id === branchKeyId) { + return deepCopyBranchKeyMaterial(activeBranchKeyMaterial) + } else { + throw new Error( + `A branch key record with branch-key-id=${id} and type=branch:ACTIVE was not found in DynamoDB` + ) + } + }) + + keyStore.getBranchKeyVersion.callsFake(async function ( + id: string, + branchKeyVersion: string + ) { + if (branchKeyId === id && branchKeyVersion === activeVersion) { + kmsSendSpy.callCount += 1 + ddbSendSpy.callCount += 1 + return deepCopyBranchKeyMaterial(versionedBranchKeyMaterial) + } else { + ddbSendSpy.callCount += 1 + throw new Error( + `A branch key record with branch-key-id=${id} and type=branch:version:${branchKeyVersion} was not found in DynamoDB` + ) + } + }) + + keyStore.getKeyStoreInfo.callsFake(function(): KeyStoreInfoOutput { + return { + keystoreId: "keyStoreId", + keystoreTableName: "keystoreTableName", + logicalKeyStoreName: "logicalKeyStoreName", + grantTokens: [], + // This is not used by any tests + kmsConfiguration: null as any + } + }) + }) + + afterEach(() => { + keyStore.getActiveBranchKey.reset() + keyStore.getBranchKeyVersion.reset() + kmsSendSpy.restore() + ddbSendSpy.restore() + }) + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#ondecrypt + //= type=test + //# The set of encrypted data keys MUST first be filtered to match this keyring’s configuration. For the encrypted data key to match: + //# - Its provider ID MUST match the UTF8 Encoded value of “aws-kms-hierarchy”. + //# - Deserialize the key provider info, if deserialization fails the next EDK in the set MUST be attempted. + //# - The deserialized key provider info MUST be UTF8 Decoded and MUST match this keyring's configured `Branch Key Identifier`. + it('Precondition: There must be an encrypted data key that matches this keyring configuration', async () => { + const hkr = new KmsHierarchicalKeyRingNode({ + branchKeyIdSupplier, + keyStore, + cacheLimitTtl, + }) + const badEdkProviderId = `bad-${PROVIDER_ID_HIERARCHY}` + + const badEdks: EncryptedDataKey[] = [ + ...Array(5).fill( + new EncryptedDataKey({ + providerInfo: BRANCH_KEY_ID_A, + providerId: badEdkProviderId, + encryptedDataKey: malformedEdkCiphertext, + }) + ), + // onDecrypt wants to use branch key A for unwrapping. Edks with provider + // info of branch key id B will not match the keyring configuration and + // fail the filter + ...Array(5).fill( + new EncryptedDataKey({ + providerInfo: BRANCH_KEY_ID_B, + providerId: PROVIDER_ID_HIERARCHY, + encryptedDataKey: malformedEdkCiphertext, + }) + ), + ] + + const decryptionMaterial = new NodeDecryptionMaterial( + TEST_ESDK_ALG_SUITE, + EC_A // use branch key A for decryption + ) + + await testOnDecryptError( + hkr, + badEdks, + decryptionMaterial, + "There must be an encrypted data key that matches this keyring's configuration" + ) + }) + + it('Precondition: The edk ciphertext must have the correct length', async () => { + // this test is already covered in the test after, but is here for + // precondition compliance checks + + const hkr = new KmsHierarchicalKeyRingNode({ + branchKeyId, + keyStore, + cacheLimitTtl, + }) + const decryptionMaterial = new NodeDecryptionMaterial( + TEST_ESDK_ALG_SUITE, + DEFAULT_EC + ) + const expectedError = `The encrypted data key ciphertext must be ${ciphertextLength} bytes long` + + await testOnDecryptError(hkr, [badEdks[0]], decryptionMaterial, undefined, [ + expectedError, + ]) + }) + + it('None of the edks can be decrypted', async () => { + const hkr = new KmsHierarchicalKeyRingNode({ + branchKeyId, + keyStore, + cacheLimitTtl, + }) + const decryptionMaterial = new NodeDecryptionMaterial( + TEST_ESDK_ALG_SUITE, + DEFAULT_EC + ) + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#ondecrypt + //= type=test + //# For each encrypted data key in the filtered set, one at a time, OnDecrypt MUST attempt to decrypt the encrypted data key. + //# If this attempt results in an error, then these errors MUST be collected. + const expectedErrors = [ + `Error #1 \n Error: The encrypted data key ciphertext must be ${ciphertextLength} bytes long`, + 'Error #2 \n Error: Input must represent a uuidv4', + `Error #3 \n Error: A branch key record with branch-key-id=${branchKeyId} and type=branch:version:${nonExistentBranchKeyVersion} was not found in DynamoDB`, + 'Error #4 \n Error: Unsupported state or unable to authenticate data', + ] + + await testOnDecryptError( + hkr, + badEdks, + decryptionMaterial, + undefined, + expectedErrors + ) + }) + + it('short circuit on the first success', async () => { + const hkr = new KmsHierarchicalKeyRingNode({ + branchKeyId, + keyStore, + cacheLimitTtl, + }) + + const goodEdks = [] + const pdks = [] + + // for every bad edk, we make a "good" decryptable edk + // by the end of these onEncrypt calls, the active branch key material will + // be in the CMC + for (let i = 0; i < badEdks.length; i++) { + // create fresh encryption material such that a different pdk will be + // generated, giving us a different edk each time + const encryptionMaterial = new NodeEncryptionMaterial( + TEST_ESDK_ALG_SUITE, + DEFAULT_EC + ) + + await testOnEncrypt(hkr, branchKeyId, encryptionMaterial) + + const edk = encryptionMaterial.encryptedDataKeys[0] + const generatedPdk = unwrapDataKey( + encryptionMaterial.getUnencryptedDataKey() + ) + + goodEdks.push(edk) + pdks.push(generatedPdk) + } + + const edks = [...badEdks, ...goodEdks] + const decryptionMaterial = new NodeDecryptionMaterial( + TEST_ESDK_ALG_SUITE, + DEFAULT_EC + ) + + // the first two bad edks won't even make it past the filter + + // the 3rd bad edk will attempt to get nonexistent versioned branch key + // material from DDB but fail (1 DDB call, 0 KMS calls) + + // the 4th bad edk will get the versioned branch key material and cache it + // but fail at unwrapping (1 more DDB call, 1 more KMS call) + + // then the 1st good edk wants the same versioned branch key material, so it + // gets the materail from the cache (0 more DDB calls, 0 more KMS calls). + // This will be a successful decryption and we short circuit. + + // the other good edks won't even be attempted because of short circuiting. + // Thus the pdk in the decryption material should match the first generated pdk + await testOnDecrypt(hkr, pdks[0], edks, branchKeyId, decryptionMaterial) + + expect(kmsSendSpy.callCount).equals(1) + expect(ddbSendSpy.callCount).equals(2) + }) +}) diff --git a/modules/kms-keyring-node/test/kms_hkeyring_node.helpers.test.ts b/modules/kms-keyring-node/test/kms_hkeyring_node.helpers.test.ts new file mode 100644 index 000000000..fc954d036 --- /dev/null +++ b/modules/kms-keyring-node/test/kms_hkeyring_node.helpers.test.ts @@ -0,0 +1,269 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { randomBytes } from 'crypto' +import { + wrapAad, + destructureCiphertext, + serializeEncryptionContext, + unwrapEncryptedDataKey, + wrapPlaintextDataKey, +} from '../src/kms_hkeyring_node_helpers' +import { expect } from 'chai' +import { PROVIDER_ID_HIERARCHY_AS_BYTES } from '../src/constants' +import { ALG_SUITES } from './fixtures' +import { + NodeBranchKeyMaterial, + NodeDecryptionMaterial, + NodeEncryptionMaterial, +} from '@aws-crypto/material-management' +import { v4 } from 'uuid' + +describe('KmsHierarchicalKeyRingNode: helpers', () => { + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#ciphertext + //= type=test + //# The following table describes the fields that form the ciphertext for this keyring. + //# The bytes are appended in the order shown. + //# The Encryption Key is variable. + //# It will be whatever length is represented by the algorithm suite. + //# Because all the other values are constant, + //# this variability in the encryption key does not impact the format. + //# | Field | Length (bytes) | Interpreted as | + //# | ------------------ | -------------- | -------------- | + //# | Salt | 16 | bytes | + //# | IV | 12 | bytes | + //# | Version | 16 | bytes | + //# | Encrypted Key | Variable | bytes | + //# | Authentication Tag | 16 | bytes | + describe('Ciphertext destructuring', () => { + it('All parts destructured correctly for all algorithm suites', () => { + for (const algSuite of ALG_SUITES) { + const actualSalt = randomBytes(16) + const actualIv = randomBytes(12) + const actualBranchKeyVersionAsBytesCompressed = randomBytes(16) + const actualEncryptedDataKey = randomBytes(algSuite.keyLengthBytes) + const actualAuthTag = randomBytes(16) + const ciphertext = Buffer.concat([ + actualSalt, + actualIv, + actualBranchKeyVersionAsBytesCompressed, + actualEncryptedDataKey, + actualAuthTag, + ]) + + // all parts correct + const { + salt, + iv, + branchKeyVersionAsBytesCompressed, + encryptedDataKey, + authTag, + } = destructureCiphertext(ciphertext, algSuite) + + expect(salt).to.deep.equal(actualSalt) + expect(iv).to.deep.equal(actualIv) + expect(branchKeyVersionAsBytesCompressed).to.deep.equal( + actualBranchKeyVersionAsBytesCompressed + ) + expect(encryptedDataKey).to.deep.equal(actualEncryptedDataKey) + expect(authTag).to.deep.equal(actualAuthTag) + + // expect error to destructure a bad length ciphertext + const badEncryptedDataKey = randomBytes(algSuite.keyLengthBytes + 1) + const badCiphertext = Buffer.concat([ + actualSalt, + actualIv, + actualBranchKeyVersionAsBytesCompressed, + badEncryptedDataKey, + actualAuthTag, + ]) + expect(() => destructureCiphertext(badCiphertext, algSuite)).to.throw( + `The encrypted data key ciphertext must be ${ + badCiphertext.length - 1 + } bytes long` + ) + } + }) + }) + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#branch-key-wrapping-and-unwrapping-aad + //= type=test + //# To Encrypt and Decrypt the `wrappedDerivedBranchKey` the keyring MUST include the following values as part of the AAD for + //# the AES Encrypt/Decrypt calls. + //# To construct the AAD, the keyring MUST concatenate the following values + //# 1. "aws-kms-hierarchy" as UTF8 Bytes + //# 1. Value of `branch-key-id` as UTF8 Bytes + //# 1. [version](../structures.md#branch-key-version) as Bytes + //# 1. [encryption context](structures.md#encryption-context-1) from the input + //# [encryption materials](../structures.md#encryption-materials) according to the [encryption context serialization specification](../structures.md#serialization). + //# | Field | Length (bytes) | Interpreted as | + //# | ------------------- | -------------- | ---------------------------------------------------- | + //# | "aws-kms-hierarchy" | 17 | UTF-8 Encoded | + //# | branch-key-id | Variable | UTF-8 Encoded | + //# | version | 16 | Bytes | + //# | encryption context | Variable | [Encryption Context](../structures.md#serialization) | + //# If the keyring cannot serialize the encryption context, the operation MUST fail. + describe('AAD construction', () => { + const branchKeyIdAsBytes = Buffer.from('myId', 'utf-8') + const branchKeyVersionAsBytes = randomBytes(16) + const encryptionContext = { + key: 'value', + } + + it('Precondition: Branch key version must be 16 bytes ', () => { + const badVersion = randomBytes(15) + expect(() => + wrapAad(branchKeyIdAsBytes, badVersion, encryptionContext) + ).to.throw('Branch key version must be 16 bytes') + }) + + it('Failed encryption context serialization', () => { + const unserializeableEc = { + 1: [], + 4: {}, + '': undefined, + } + + expect(() => + wrapAad( + branchKeyIdAsBytes, + branchKeyVersionAsBytes, + unserializeableEc as any + ) + ).to.throw() + }) + + it('Ensure AAD structure', () => { + const wrappedAad = wrapAad( + branchKeyIdAsBytes, + branchKeyVersionAsBytes, + encryptionContext + ) + + let startIdx = 0 + expect(wrappedAad.subarray(startIdx, startIdx + 17)).to.deep.equal( + PROVIDER_ID_HIERARCHY_AS_BYTES + ) + + startIdx += 17 + expect( + wrappedAad.subarray(startIdx, startIdx + branchKeyIdAsBytes.length) + ).to.deep.equal(branchKeyIdAsBytes) + + startIdx += branchKeyIdAsBytes.length + expect( + wrappedAad.subarray(startIdx, startIdx + branchKeyVersionAsBytes.length) + ).to.deep.equal(branchKeyVersionAsBytes) + + startIdx += branchKeyVersionAsBytes.length + const expectedAad = serializeEncryptionContext(encryptionContext).slice(2) + expect(wrappedAad.subarray(startIdx)).to.deep.equal(expectedAad) + }) + }) + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#branch-key-wrapping + //= type=test + //# To derive and encrypt a data key the keyring will follow the same key derivation and encryption as [AWS KMS](https://rwc.iacr.org/2018/Slides/Gueron.pdf). + //# The hierarchical keyring MUST: + //# 1. Generate a 16 byte random `salt` using a secure source of randomness + //# 1. Generate a 12 byte random `IV` using a secure source of randomness + //# 1. Use a [KDF in Counter Mode with a Pseudo Random Function with HMAC SHA 256](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-108r1.pdf) to derive a 32 byte `derivedBranchKey` data key with the following inputs: + //# - Use the `salt` as the salt. + //# - Use the branch key as the `key`. + //# - Use the UTF8 Encoded value "aws-kms-hierarchy" as the label. + //# 1. Encrypt a plaintext data key with the `derivedBranchKey` using `AES-GCM-256` with the following inputs: + //# - MUST use the `derivedBranchKey` as the AES-GCM cipher key. + //# - MUST use the plain text data key that will be wrapped by the `derivedBranchKey` as the AES-GCM message. + //# - MUST use the derived `IV` as the AES-GCM IV. + //# - MUST use an authentication tag byte of length 16. + //# - MUST use the serialized [AAD](#branch-key-wrapping-and-unwrapping-aad) as the AES-GCM AAD. + //# If OnEncrypt fails to do any of the above, OnEncrypt MUST fail. + describe('Wrapping plaintext data key', () => { + it('The ciphertext can be deciphered for all algorithm suites', () => { + for (const algSuite of ALG_SUITES) { + const expectedPdk = randomBytes(algSuite.keyLengthBytes) + const branchKey = Buffer.alloc(32) + const branchKeyId = 'myBranchKey' + const branchKeyVersion = v4() + const encryptionContext = { key: 'value' } + const branchKeyMaterial = new NodeBranchKeyMaterial( + branchKey, + branchKeyId, + branchKeyVersion, + encryptionContext + ) + const encryptionMaterial = new NodeEncryptionMaterial( + algSuite, + encryptionContext + ) + const decryptionMaterial = new NodeDecryptionMaterial( + algSuite, + encryptionContext + ) + + const actualPdk = unwrapEncryptedDataKey( + wrapPlaintextDataKey( + expectedPdk, + branchKeyMaterial, + encryptionMaterial + ), + branchKeyMaterial, + decryptionMaterial + ) + expect(actualPdk).to.deep.equal(expectedPdk) + } + }) + }) + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#branch-key-unwrapping + //= type=test + //# To decrypt an encrypted data key with a branch key, the hierarchical keyring MUST: + //# 1. Deserialize the 16 byte random `salt` from the [edk ciphertext](../structures.md#ciphertext). + //# 1. Deserialize the 12 byte random `IV` from the [edk ciphertext](../structures.md#ciphertext). + //# 1. Deserialize the 16 byte `version` from the [edk ciphertext](../structures.md#ciphertext). + //# 1. Deserialize the `encrypted key` from the [edk ciphertext](../structures.md#ciphertext). + //# 1. Deserialize the `authentication tag` from the [edk ciphertext](../structures.md#ciphertext). + //# 1. Use a [KDF in Counter Mode with a Pseudo Random Function with HMAC SHA 256](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-108r1.pdf) to derive + //# the 32 byte `derivedBranchKey` data key with the following inputs: + //# - Use the `salt` as the salt. + //# - Use the branch key as the `key`. + //# 1. Decrypt the encrypted data key with the `derivedBranchKey` using `AES-GCM-256` with the following inputs: + //# - It MUST use the `encrypted key` obtained from deserialization as the AES-GCM input ciphertext. + //# - It MUST use the `authentication tag` obtained from deserialization as the AES-GCM input authentication tag. + //# - It MUST use the `derivedBranchKey` as the AES-GCM cipher key. + //# - It MUST use the `IV` obtained from deserialization as the AES-GCM input IV. + //# - It MUST use the serialized [encryption context](#branch-key-wrapping-and-unwrapping-aad) as the AES-GCM AAD. + //# If OnDecrypt fails to do any of the above, OnDecrypt MUST fail. + describe('Encrypted data key unwrapping', () => { + it('Error with creating the decipher for all algorithm suites', () => { + for (const algSuite of ALG_SUITES) { + // create a ciphertext that can be destructured but not deciphered + const ciphertext = randomBytes( + 16 + 12 + 16 + algSuite.keyLengthBytes + 16 + ) + const branchKey = Buffer.alloc(32) + const branchKeyId = 'myBranchKey' + const branchKeyVersion = v4() + const encryptionContext = { key: 'value' } + const branchKeyMaterial = new NodeBranchKeyMaterial( + branchKey, + branchKeyId, + branchKeyVersion, + encryptionContext + ) + const decryptionMaterial = new NodeDecryptionMaterial( + algSuite, + encryptionContext + ) + + expect(() => + unwrapEncryptedDataKey( + ciphertext, + branchKeyMaterial, + decryptionMaterial + ) + ).to.throw('Unsupported state or unable to authenticate data') + } + }) + }) +}) diff --git a/modules/kms-keyring-node/test/kms_hkeyring_node.ondecrypt.test.ts b/modules/kms-keyring-node/test/kms_hkeyring_node.ondecrypt.test.ts new file mode 100644 index 000000000..2c9f5b809 --- /dev/null +++ b/modules/kms-keyring-node/test/kms_hkeyring_node.ondecrypt.test.ts @@ -0,0 +1,658 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + EncryptedDataKey, + NodeBranchKeyMaterial, + NodeDecryptionMaterial, + NodeEncryptionMaterial, + unwrapDataKey, +} from '@aws-crypto/material-management' +import { + ALG_SUITES, + BRANCH_KEY_ID_A, + BRANCH_KEY_ID_B, + DEFAULT_EC, + EC_A, + EC_B, + KEYSTORE, + TEST_ESDK_ALG_SUITE, + TTL, +} from './fixtures' +import { + IKmsHierarchicalKeyRingNode, + KmsHierarchicalKeyRingNode, +} from '../src/kms_hkeyring_node' +import { + BRANCH_KEY_ID_SUPPLIER, + deepCopyBranchKeyMaterial, + testOnDecrypt, + testOnDecryptError, + testOnEncrypt, +} from './kms_hkeyring_node.test' +import { expect } from 'chai' +import Sinon from 'sinon' +import { KMSClient } from '@aws-sdk/client-kms' +import { DynamoDBClient } from '@aws-sdk/client-dynamodb' +import { BranchKeyStoreNode, KeyStoreInfoOutput } from '@aws-crypto/branch-keystore-node' + +const branchKeyIdA = BRANCH_KEY_ID_A +const branchKeyIdB = BRANCH_KEY_ID_B +const branchKeyIdSupplier = BRANCH_KEY_ID_SUPPLIER +const originalKeyStore = KEYSTORE +const cacheLimitTtl = TTL + +async function generatePdkAndEdks( + hkr: IKmsHierarchicalKeyRingNode, + wrappingKeyName: string, + encryptionMaterial: NodeEncryptionMaterial +) { + await testOnEncrypt(hkr, wrappingKeyName, encryptionMaterial) + + const encryptedPdk = unwrapDataKey(encryptionMaterial.getUnencryptedDataKey()) + const edks = encryptionMaterial.encryptedDataKeys + + return { encryptedPdk, edks } +} + +let versionedBranchKeyMaterialA: NodeBranchKeyMaterial +let versionedBranchKeyMaterialB: NodeBranchKeyMaterial +let activeVersionA: string +let activeVersionB: string +let activeBranchKeyMaterialA: NodeBranchKeyMaterial +let activeBranchKeyMaterialB: NodeBranchKeyMaterial +before(async function () { + activeBranchKeyMaterialA = await originalKeyStore.getActiveBranchKey( + branchKeyIdA + ) + activeVersionA = activeBranchKeyMaterialA.branchKeyVersion.toString('utf-8') + + activeBranchKeyMaterialB = await originalKeyStore.getActiveBranchKey( + branchKeyIdB + ) + activeVersionB = activeBranchKeyMaterialB.branchKeyVersion.toString('utf-8') + + versionedBranchKeyMaterialA = await originalKeyStore.getBranchKeyVersion( + branchKeyIdA, + activeVersionA + ) + + versionedBranchKeyMaterialB = await originalKeyStore.getBranchKeyVersion( + branchKeyIdB, + activeVersionB + ) +}) + +describe('KmsHierarchicalKeyRingNode: onDecrypt', () => { + let keyStore: Sinon.SinonStubbedInstance + let kmsSendSpy: Sinon.SinonSpy + let ddbSendSpy: Sinon.SinonSpy + let clock: Sinon.SinonFakeTimers + + beforeEach(() => { + keyStore = Sinon.createStubInstance(BranchKeyStoreNode) + kmsSendSpy = Sinon.spy(KMSClient.prototype, 'send') + ddbSendSpy = Sinon.spy(DynamoDBClient.prototype, 'send') + clock = Sinon.useFakeTimers() + + keyStore.getActiveBranchKey.callsFake(async function (branchKeyId: string) { + if (branchKeyId === branchKeyIdA) { + return deepCopyBranchKeyMaterial(activeBranchKeyMaterialA) + } else if (branchKeyId === branchKeyIdB) { + return deepCopyBranchKeyMaterial(activeBranchKeyMaterialB) + } else { + throw new Error( + `A branch key record with branch-key-id=${branchKeyId} and type=branch:ACTIVE was not found in DynamoDB` + ) + } + }) + + keyStore.getBranchKeyVersion.callsFake(async function ( + branchKeyId: string, + branchKeyVersion: string + ) { + if (branchKeyId === branchKeyIdA && branchKeyVersion === activeVersionA) { + kmsSendSpy.callCount += 1 + ddbSendSpy.callCount += 1 + return deepCopyBranchKeyMaterial(versionedBranchKeyMaterialA) + } else if ( + branchKeyId === branchKeyIdB && + branchKeyVersion === activeVersionB + ) { + kmsSendSpy.callCount += 1 + ddbSendSpy.callCount += 1 + return deepCopyBranchKeyMaterial(versionedBranchKeyMaterialB) + } else { + ddbSendSpy.callCount += 1 + throw new Error( + `A branch key record with branch-key-id=${branchKeyId} and type=branch:version:${branchKeyVersion} was not found in DynamoDB` + ) + } + }) + + keyStore.getKeyStoreInfo.callsFake(function(): KeyStoreInfoOutput { + return { + keystoreId: "keyStoreId", + keystoreTableName: "keystoreTableName", + logicalKeyStoreName: "logicalKeyStoreName", + grantTokens: [], + // This is not used by any tests + kmsConfiguration: null as any + } + }) + }) + + afterEach(() => { + keyStore.getActiveBranchKey.reset() + keyStore.getBranchKeyVersion.reset() + kmsSendSpy.restore() + ddbSendSpy.restore() + clock.restore() + }) + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#ondecrypt + //= type=test + //# The `branchKeyId` used in this operation is either the configured branchKeyId, if supplied, or the result of the `branchKeySupplier`'s + //# `getBranchKeyId` operation, using the decryption material's encryption context as input. + it('Uses either the branch key id or supplier', async () => { + let hkr = new KmsHierarchicalKeyRingNode({ + branchKeyIdSupplier, + keyStore, + cacheLimitTtl, + }) + let { encryptedPdk, edks } = await generatePdkAndEdks( + hkr, + branchKeyIdA, + new NodeEncryptionMaterial(TEST_ESDK_ALG_SUITE, EC_A) + ) + + await testOnDecrypt( + hkr, + encryptedPdk, + edks, + branchKeyIdA, + new NodeDecryptionMaterial(TEST_ESDK_ALG_SUITE, EC_A) + ) + + hkr = new KmsHierarchicalKeyRingNode({ + branchKeyId: branchKeyIdA, + keyStore, + cacheLimitTtl, + }) + + const result = await generatePdkAndEdks( + hkr, + branchKeyIdA, + new NodeEncryptionMaterial(TEST_ESDK_ALG_SUITE, DEFAULT_EC) + ) + encryptedPdk = result.encryptedPdk + edks = result.edks + + await testOnDecrypt( + hkr, + encryptedPdk, + edks, + branchKeyIdA, + new NodeDecryptionMaterial(TEST_ESDK_ALG_SUITE, DEFAULT_EC) + ) + }) + + it('Error in the branch key id supplier leads to operation failure', async () => { + const hkr = new KmsHierarchicalKeyRingNode({ + branchKeyIdSupplier, + keyStore, + cacheLimitTtl, + }) + const decryptionMaterial = new NodeDecryptionMaterial( + TEST_ESDK_ALG_SUITE, + DEFAULT_EC + ) + const { edks } = await generatePdkAndEdks( + hkr, + branchKeyIdA, + new NodeEncryptionMaterial(TEST_ESDK_ALG_SUITE, EC_A) + ) + + await testOnDecryptError( + hkr, + edks, + decryptionMaterial, + "Can't determine branchKeyId from context" + ) + }) + + describe('Setting the pdk after edk decryption', () => { + it('Decryption material has a pdk that is set and later zeroed out', async () => { + const hkr = new KmsHierarchicalKeyRingNode({ + branchKeyId: branchKeyIdA, + keyStore, + cacheLimitTtl, + }) + const decryptionMaterial = new NodeDecryptionMaterial( + TEST_ESDK_ALG_SUITE, + DEFAULT_EC + ) + const { encryptedPdk, edks } = await generatePdkAndEdks( + hkr, + branchKeyIdA, + new NodeEncryptionMaterial(TEST_ESDK_ALG_SUITE, DEFAULT_EC) + ) + + // this first decryption will set the pdk + await testOnDecrypt( + hkr, + encryptedPdk, + edks, + branchKeyIdA, + decryptionMaterial + ) + + // then we zero out the pdk, rendering the decryption material dead + decryptionMaterial.zeroUnencryptedDataKey() + + // then we should fail to decrypt the edks this time + await testOnDecryptError( + hkr, + edks, + decryptionMaterial, + 'unencryptedDataKey has already been set' + ) + }) + + it('Decryption material has a pdk that is not set and immediately zeroed out', async () => { + const hkr = new KmsHierarchicalKeyRingNode({ + branchKeyId: branchKeyIdA, + keyStore, + cacheLimitTtl, + }) + const decryptionMaterial = new NodeDecryptionMaterial( + TEST_ESDK_ALG_SUITE, + DEFAULT_EC + ).zeroUnencryptedDataKey() + const { edks } = await generatePdkAndEdks( + hkr, + branchKeyIdA, + new NodeEncryptionMaterial(TEST_ESDK_ALG_SUITE, DEFAULT_EC) + ) + + await testOnDecryptError( + hkr, + edks, + decryptionMaterial, + 'unencryptedDataKey has already been set' + ) + }) + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#ondecrypt + //= type=test + //# If the decryption materials already contain a `PlainTextDataKey`, OnDecrypt MUST fail. + it('Precondition: If the decryption materials already contain a PlainTextDataKey, OnDecrypt MUST fail', async () => { + const hkr = new KmsHierarchicalKeyRingNode({ + branchKeyId: branchKeyIdA, + keyStore, + cacheLimitTtl, + }) + const decryptionMaterial = new NodeDecryptionMaterial( + TEST_ESDK_ALG_SUITE, + DEFAULT_EC + ) + const { encryptedPdk, edks } = await generatePdkAndEdks( + hkr, + branchKeyIdA, + new NodeEncryptionMaterial(TEST_ESDK_ALG_SUITE, DEFAULT_EC) + ) + + // this first decryption will set the pdk + await testOnDecrypt( + hkr, + encryptedPdk, + edks, + branchKeyIdA, + decryptionMaterial + ) + + // then we should fail to decrypt the edks this time + await testOnDecryptError( + hkr, + edks, + decryptionMaterial, + 'Decryption materials already contain a plaintext data key' + ) + }) + + it('Correct length pdk is decrypted for all algorithm suites', async () => { + const hkr = new KmsHierarchicalKeyRingNode({ + branchKeyId: branchKeyIdA, + keyStore, + cacheLimitTtl, + }) + + for (const algSuite of ALG_SUITES) { + const { encryptedPdk, edks } = await generatePdkAndEdks( + hkr, + branchKeyIdA, + new NodeEncryptionMaterial(algSuite, DEFAULT_EC) + ) + + await testOnDecrypt( + hkr, + encryptedPdk, + edks, + branchKeyIdA, + new NodeDecryptionMaterial(algSuite, DEFAULT_EC) + ) + } + }) + }) + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#ondecrypt + //= type=test + //# If a cache entry is found and the entry's TTL has not expired, the hierarchical keyring MUST use those branch key materials for key unwrapping. + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#ondecrypt + //# If a cache entry is not found or the cache entry is expired, the hierarchical keyring + //# MUST attempt to obtain the branch key materials by calling the backing branch key + //# store specified in the [retrieve OnDecrypt branch key materials](#getitem-branch-keystore-ondecrypt) section. + //# If the keyring is not able to retrieve `branch key materials` from the backing keystore then OnDecrypt MUST fail. + describe('Getting the branch key material', () => { + it('Material X not already in the CMC or keystore, request material X', async () => { + let hkr = new KmsHierarchicalKeyRingNode({ + branchKeyId: branchKeyIdA, + keyStore, + cacheLimitTtl, + }) + const { edks } = await generatePdkAndEdks( + hkr, + branchKeyIdA, + new NodeEncryptionMaterial(TEST_ESDK_ALG_SUITE, DEFAULT_EC) + ) + + // modify the edks such that their wrapping key name is not the existent + // branch key id that they were originally wrapped with + const nonexistentBranchKeyId = 'lol' + const modifiedEdks = edks.map( + (edk) => + new EncryptedDataKey({ + ...edk, + providerInfo: nonexistentBranchKeyId, + }) + ) + + hkr = new KmsHierarchicalKeyRingNode({ + branchKeyId: nonexistentBranchKeyId, // so that the edks match the keyring configuration and pass the filter + keyStore, + cacheLimitTtl, + }) + + const decyrptionMaterial = new NodeDecryptionMaterial( + TEST_ESDK_ALG_SUITE, + DEFAULT_EC + ) + + // now when we try to decrypt, we will get an error saying that we + // couldn't get the necessary versioned branch key material to unwrap the + // edk + await testOnDecryptError( + hkr, + modifiedEdks, + decyrptionMaterial, + undefined, + [ + `A branch key record with branch-key-id=${nonexistentBranchKeyId} and type=branch:version:${activeVersionA} was not found in DynamoDB`, + ] + ) + + expect(ddbSendSpy.callCount).equals(1) + expect(kmsSendSpy.callCount).equals(0) + }) + + it('Material X not already in CMC, request for Material X', async () => { + const hkr = new KmsHierarchicalKeyRingNode({ + branchKeyId: branchKeyIdA, + keyStore, + cacheLimitTtl, + }) + const decryptionMaterial = new NodeDecryptionMaterial( + TEST_ESDK_ALG_SUITE, + DEFAULT_EC + ) + const { encryptedPdk, edks } = await generatePdkAndEdks( + hkr, + branchKeyIdA, + new NodeEncryptionMaterial(TEST_ESDK_ALG_SUITE, DEFAULT_EC) + ) + + // the versioned branch key material that we want is not in the CMC, so we + // get it from the keystore + await testOnDecrypt( + hkr, + encryptedPdk, + edks, + branchKeyIdA, + decryptionMaterial + ) + expect(ddbSendSpy.callCount).equals(1) + expect(kmsSendSpy.callCount).equals(1) + }) + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#getitem-branch-keystore-ondecrypt + //= type=test + //# The branch keystore persists [branch keys](#definitions) that are reused to derive unique data keys for key wrapping to + //# reduce the number of calls to AWS KMS through the use of the + //# [cryptographic materials cache](../cryptographic-materials-cache.md). + //# OnDecrypt MUST calculate the following values: + //# - Deserialize the UTF8-Decoded `branch-key-id` from the [key provider info](../structures.md#key-provider-information) of the [encrypted data key](../structures.md#encrypted-data-key) + //# and verify this is equal to the configured or supplied `branch-key-id`. + //# - Deserialize the UUID string representation of the `version` from the [encrypted data key](../structures.md#encrypted-data-key) [ciphertext](#ciphertext). + //# OnDecrypt MUST call the Keystore's [GetBranchKeyVersion](../branch-key-store.md#getbranchkeyversion) operation with the following inputs: + //# - The deserialized, UTF8-Decoded `branch-key-id` + //# - The deserialized UUID string representation of the `version` + //# If the Keystore's GetBranchKeyVersion operation succeeds + //# the keyring MUST put the returned branch key materials in the cache using the + //# formula defined in [Appendix A](#appendix-a-cache-entry-identifier-formulas). + //# Otherwise, OnDecrypt MUST fail. + it('Material X already in CMC, request for Material X', async () => { + const hkr = new KmsHierarchicalKeyRingNode({ + branchKeyId: branchKeyIdA, + keyStore, + cacheLimitTtl, + }) + const decyrptionMaterial = new NodeDecryptionMaterial( + TEST_ESDK_ALG_SUITE, + DEFAULT_EC + ) + const { encryptedPdk, edks } = await generatePdkAndEdks( + hkr, + branchKeyIdA, + new NodeEncryptionMaterial(TEST_ESDK_ALG_SUITE, DEFAULT_EC) + ) + + await testOnDecrypt( + hkr, + encryptedPdk, + edks, + branchKeyIdA, + new NodeDecryptionMaterial(TEST_ESDK_ALG_SUITE, DEFAULT_EC) + ) + expect(kmsSendSpy.callCount).equals(1) + expect(ddbSendSpy.callCount).equals(1) + + // the versioned branch key material that we want is already in the CMC, + // so we don't need to call the keystore + await testOnDecrypt( + hkr, + encryptedPdk, + edks, + branchKeyIdA, + decyrptionMaterial + ) + + expect(kmsSendSpy.callCount).equals(1) + expect(ddbSendSpy.callCount).equals(1) + }) + + it('Material A already in the CMC, ask for material B in keystore', async () => { + const hkr = new KmsHierarchicalKeyRingNode({ + branchKeyIdSupplier, + keyStore, + cacheLimitTtl, + }) + // create decryption materials to tell onDecrypt to attempt decrypting the + // edks by unwrapping with branch key A + const decryptionMaterialA = new NodeDecryptionMaterial( + TEST_ESDK_ALG_SUITE, + EC_A + ) + // create decryption materials to tell onDecrypt to attempt decrypting the + // edks by unwrapping with branch key A + const decryptionMaterialB = new NodeDecryptionMaterial( + TEST_ESDK_ALG_SUITE, + EC_B + ) + + const { encryptedPdk: encryptedPdkA, edks: edksA } = + await generatePdkAndEdks( + hkr, + branchKeyIdA, + new NodeEncryptionMaterial(TEST_ESDK_ALG_SUITE, EC_A) + ) + const { encryptedPdk: encryptedPdkB, edks: edksB } = + await generatePdkAndEdks( + hkr, + branchKeyIdB, + new NodeEncryptionMaterial(TEST_ESDK_ALG_SUITE, EC_B) + ) + + // use branch key A to decrypt the edks that were wrapped with branch key + // A. This calls the keystore to get the versioned branch key material A, + // and puts it into the CMC + await testOnDecrypt( + hkr, + encryptedPdkA, + edksA, + branchKeyIdA, + decryptionMaterialA + ) + expect(kmsSendSpy.callCount).equals(1) + expect(ddbSendSpy.callCount).equals(1) + + // use branch key B to decrypt the edks that were wrapped with branch key + // B. This calls the keystore to get the versioned branch key material B + // because it is not in the CMC. Only versioned branch key material A is + // in the CMC + await testOnDecrypt( + hkr, + encryptedPdkB, + edksB, + branchKeyIdB, + decryptionMaterialB + ) + expect(kmsSendSpy.callCount).equals(2) + expect(ddbSendSpy.callCount).equals(2) + }) + + it('CMC evictions occur due to long network calls', async () => { + const cacheLimitTtl = 10 / 1000 // set to 10 ms + const hkr = new KmsHierarchicalKeyRingNode({ + branchKeyId: branchKeyIdA, + keyStore, + cacheLimitTtl, + }) + const { encryptedPdk, edks } = await generatePdkAndEdks( + hkr, + branchKeyIdA, + new NodeEncryptionMaterial(TEST_ESDK_ALG_SUITE, DEFAULT_EC) + ) + + // To decrypt the edks, we need to get branch key material from the + // keystore + await testOnDecrypt( + hkr, + encryptedPdk, + edks, + branchKeyIdA, + new NodeDecryptionMaterial(TEST_ESDK_ALG_SUITE, DEFAULT_EC) + ) + expect(kmsSendSpy.callCount).equals(1) + expect(ddbSendSpy.callCount).equals(1) + + // stall for twice TTL such that we evict the branch key material that we just put in the cmc + // from the previous decrypt call + clock.tick(cacheLimitTtl * 2 * 1000) + + // now we attempt to decrypt using the same branch key material again. + // However, it is not in the CMC so we must call the keystore again + await testOnDecrypt( + hkr, + encryptedPdk, + edks, + branchKeyIdA, + new NodeDecryptionMaterial(TEST_ESDK_ALG_SUITE, DEFAULT_EC) + ) + expect(kmsSendSpy.callCount).equals(2) + expect(ddbSendSpy.callCount).equals(2) + }) + + it('CMC evictions occur due to capacity', async () => { + const maxCacheSize = 1 + + const hkr = new KmsHierarchicalKeyRingNode({ + branchKeyIdSupplier, + keyStore, + cacheLimitTtl, + maxCacheSize, + }) + + const { encryptedPdk: encryptedPdkA, edks: edksA } = + await generatePdkAndEdks( + hkr, + branchKeyIdA, + new NodeEncryptionMaterial(TEST_ESDK_ALG_SUITE, EC_A) + ) + const { encryptedPdk: encryptedPdkB, edks: edksB } = + await generatePdkAndEdks( + hkr, + branchKeyIdB, + new NodeEncryptionMaterial(TEST_ESDK_ALG_SUITE, EC_B) + ) + + // decrypt edks A using branch key material A. Branch key material A is + // not in the cmc yet so we call the keystore + await testOnDecrypt( + hkr, + encryptedPdkA, + edksA, + branchKeyIdA, + new NodeDecryptionMaterial(TEST_ESDK_ALG_SUITE, EC_A) + ) + expect(kmsSendSpy.callCount).equals(1) + expect(ddbSendSpy.callCount).equals(1) + + // decrypt edks B using branch key material B. Branch key material B is + // not in the cmc yet so we call the keystore. Since the cmc capacity is + // 1, this evicts the branch key material A that we just put into the cmc + // during the previous decrypt call + await testOnDecrypt( + hkr, + encryptedPdkB, + edksB, + branchKeyIdB, + new NodeDecryptionMaterial(TEST_ESDK_ALG_SUITE, EC_B) + ) + expect(kmsSendSpy.callCount).equals(2) + expect(ddbSendSpy.callCount).equals(2) + + // now we want to decrypt under branch key material A again, but it is not + // in the cmc. So we must call the keystore + await testOnDecrypt( + hkr, + encryptedPdkA, + edksA, + branchKeyIdA, + new NodeDecryptionMaterial(TEST_ESDK_ALG_SUITE, EC_A) + ) + expect(kmsSendSpy.callCount).equals(3) + expect(ddbSendSpy.callCount).equals(3) + }) + }) +}) diff --git a/modules/kms-keyring-node/test/kms_hkeyring_node.onencrypt.test.ts b/modules/kms-keyring-node/test/kms_hkeyring_node.onencrypt.test.ts new file mode 100644 index 000000000..a7407a4bc --- /dev/null +++ b/modules/kms-keyring-node/test/kms_hkeyring_node.onencrypt.test.ts @@ -0,0 +1,416 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + NodeBranchKeyMaterial, + NodeEncryptionMaterial, +} from '@aws-crypto/material-management' +import { KmsHierarchicalKeyRingNode } from '../src/kms_hkeyring_node' +import chai, { expect } from 'chai' +import { + ALG_SUITES, + BRANCH_KEY_ID_A, + BRANCH_KEY_ID_B, + DEFAULT_EC, + EC_A, + EC_B, + KEYSTORE, + TEST_ESDK_ALG_SUITE, + TTL, +} from './fixtures' +import chaiAsPromised from 'chai-as-promised' +import Sinon from 'sinon' +import { KMSClient } from '@aws-sdk/client-kms' +import { DynamoDBClient } from '@aws-sdk/client-dynamodb' +import { + BRANCH_KEY_ID_SUPPLIER, + deepCopyBranchKeyMaterial, + testOnEncrypt, + testOnEncryptError, +} from './kms_hkeyring_node.test' +import { BranchKeyStoreNode, KeyStoreInfoOutput } from '@aws-crypto/branch-keystore-node' +chai.use(chaiAsPromised) + +const branchKeyIdA = BRANCH_KEY_ID_A +const branchKeyIdB = BRANCH_KEY_ID_B +const branchKeyIdSupplier = BRANCH_KEY_ID_SUPPLIER +const originalKeyStore = KEYSTORE +const cacheLimitTtl = TTL + +// before running any tests, get the active branch key material for both branch +// key ids +let activeBranchKeyMaterialA: NodeBranchKeyMaterial +let activeBranchKeyMaterialB: NodeBranchKeyMaterial +before(async () => { + activeBranchKeyMaterialA = await originalKeyStore.getActiveBranchKey( + branchKeyIdA + ) + activeBranchKeyMaterialB = await originalKeyStore.getActiveBranchKey( + branchKeyIdB + ) +}) + +describe('KmsHierarchicalKeyRingNode: onEncrypt', () => { + // mocking the real keystore + let keyStore: Sinon.SinonStubbedInstance + let kmsSendSpy: Sinon.SinonSpy + let ddbSendSpy: Sinon.SinonSpy + let clock: Sinon.SinonFakeTimers + + // what to do before each test + beforeEach(() => { + // mock keystore + keyStore = Sinon.createStubInstance(BranchKeyStoreNode) + // spies to count network calls + kmsSendSpy = Sinon.spy(KMSClient.prototype, 'send') + ddbSendSpy = Sinon.spy(DynamoDBClient.prototype, 'send') + // a clock to simulate TTL stalls + clock = Sinon.useFakeTimers() + + // mock get active branch key material + keyStore.getActiveBranchKey.callsFake(async (branchKeyId: string) => { + if (branchKeyId === branchKeyIdA) { + kmsSendSpy.callCount += 1 + ddbSendSpy.callCount += 1 + return deepCopyBranchKeyMaterial(activeBranchKeyMaterialA) + } else if (branchKeyId === branchKeyIdB) { + kmsSendSpy.callCount += 1 + ddbSendSpy.callCount += 1 + return deepCopyBranchKeyMaterial(activeBranchKeyMaterialB) + } else { + ddbSendSpy.callCount += 1 + throw new Error( + `A branch key record with branch-key-id=${branchKeyId} and type=branch:ACTIVE was not found in DynamoDB` + ) + } + }) + + keyStore.getKeyStoreInfo.callsFake(function(): KeyStoreInfoOutput { + return { + keystoreId: "keyStoreId", + keystoreTableName: "keystoreTableName", + logicalKeyStoreName: "logicalKeyStoreName", + grantTokens: [], + // This is not used by any tests + kmsConfiguration: null as any + } + }) + }) + + // what to do after each test: reset all sinons + afterEach(() => { + keyStore.getActiveBranchKey.reset() + kmsSendSpy.restore() + ddbSendSpy.restore() + clock.restore() + }) + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#onencrypt + //= type=test + //# The `branchKeyId` used in this operation is either the configured branchKeyId, if supplied, or the result of the `branchKeySupplier`'s + //# `getBranchKeyId` operation, using the encryption material's encryption context as input. + it('Uses either the branch key id or supplier', async () => { + let hkr = new KmsHierarchicalKeyRingNode({ + branchKeyIdSupplier, + keyStore, + cacheLimitTtl, + }) + + await testOnEncrypt( + hkr, + branchKeyIdA, + new NodeEncryptionMaterial(TEST_ESDK_ALG_SUITE, EC_A) + ) + + hkr = new KmsHierarchicalKeyRingNode({ + branchKeyId: branchKeyIdA, + keyStore, + cacheLimitTtl, + }) + + await testOnEncrypt( + hkr, + branchKeyIdA, + new NodeEncryptionMaterial(TEST_ESDK_ALG_SUITE, DEFAULT_EC) + ) + }) + + it('Error in the branch key id supplier leads to operation failure', async () => { + const hkr = new KmsHierarchicalKeyRingNode({ + branchKeyIdSupplier, + keyStore, + cacheLimitTtl, + }) + + await testOnEncryptError( + hkr, + new NodeEncryptionMaterial(TEST_ESDK_ALG_SUITE, DEFAULT_EC), + "Can't determine branchKeyId from context" + ) + }) + + describe('Getting the pdk', () => { + it('Existing pdk is zeroed', async () => { + const encryptionMaterial = new NodeEncryptionMaterial( + TEST_ESDK_ALG_SUITE, + DEFAULT_EC + ) + + const hkr = new KmsHierarchicalKeyRingNode({ + branchKeyId: branchKeyIdA, + keyStore, + cacheLimitTtl, + }) + + await testOnEncrypt(hkr, branchKeyIdA, encryptionMaterial) + + // now zero it out and try + encryptionMaterial.zeroUnencryptedDataKey() + await testOnEncryptError( + hkr, + encryptionMaterial, + 'unencryptedDataKey has already been set' + ) + }) + + it('Pdk is zeroed without being set', async () => { + const encryptionMaterial = new NodeEncryptionMaterial( + TEST_ESDK_ALG_SUITE, + DEFAULT_EC + ).zeroUnencryptedDataKey() + + const hkr = new KmsHierarchicalKeyRingNode({ + branchKeyId: branchKeyIdA, + keyStore, + cacheLimitTtl, + }) + + await testOnEncryptError( + hkr, + encryptionMaterial, + 'unencryptedDataKey has already been set' + ) + }) + + it('Existing pdk is not overriden', async () => { + const encryptionMaterial = new NodeEncryptionMaterial( + TEST_ESDK_ALG_SUITE, + DEFAULT_EC + ) + const hkr = new KmsHierarchicalKeyRingNode({ + branchKeyId: branchKeyIdA, + keyStore, + cacheLimitTtl, + }) + + // one call to generate an initial pdk + await testOnEncrypt(hkr, branchKeyIdA, encryptionMaterial) + + // another call to ensure the existing pdk does not change + await testOnEncrypt(hkr, branchKeyIdA, encryptionMaterial) + }) + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#onencrypt + //= type=test + //# If the input [encryption materials](../structures.md#encryption-materials) do not contain a plaintext data key, + //# OnEncrypt MUST generate a random plaintext data key, according to the key length defined in the [algorithm suite](../algorithm-suites.md#encryption-key-length). + //# The process used to generate this random plaintext data key MUST use a secure source of randomness. + it('Correct length pdk is generated for all algorithm suites', async () => { + const hkr = new KmsHierarchicalKeyRingNode({ + branchKeyId: branchKeyIdA, + keyStore, + cacheLimitTtl, + }) + + for (const algSuite of ALG_SUITES) { + // run onEncrypt with an encryption material for each algorithm suite + await testOnEncrypt( + hkr, + branchKeyIdA, + new NodeEncryptionMaterial(algSuite, DEFAULT_EC) + ) + } + }) + }) + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#onencrypt + //= type=test + //# If a cache entry is found and the entry's TTL has not expired, the hierarchical keyring MUST use those branch key materials for key wrapping. + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#onencrypt + //# If a cache entry is not found or the cache entry is expired, the hierarchical keyring MUST attempt to obtain the branch key materials + //# by querying the backing branch keystore specified in the [retrieve OnEncrypt branch key materials](#query-branch-keystore-onencrypt) section. + //# If the keyring is not able to retrieve [branch key materials](../structures.md#branch-key-materials) + //# through the underlying cryptographic materials cache or + //# it no longer has access to them through the backing keystore, OnEncrypt MUST fail. + describe('Getting the branch key material', () => { + it('Material X not already in the CMC or keystore, request material X', async () => { + const branchKeyId = 'lol' + const hkr = new KmsHierarchicalKeyRingNode({ + branchKeyId, + keyStore, + cacheLimitTtl, + }) + const encryptionMaterial = new NodeEncryptionMaterial( + TEST_ESDK_ALG_SUITE, + DEFAULT_EC + ) + + // there is nothing in the cmc, so onEncrypt will request active branch key + // material from keystore. It will try to query DDB for 'lol' and not find + // an item + await testOnEncryptError( + hkr, + encryptionMaterial, + `A branch key record with branch-key-id=${branchKeyId} and type=branch:ACTIVE was not found in DynamoDB` + ) + expect(kmsSendSpy.callCount).equals(0) + expect(ddbSendSpy.callCount).equals(1) + }) + + it('Material X not already in CMC, request for Material X', async () => { + const hkr = new KmsHierarchicalKeyRingNode({ + branchKeyId: branchKeyIdA, + keyStore, + cacheLimitTtl, + }) + const encryptionMaterial = new NodeEncryptionMaterial( + TEST_ESDK_ALG_SUITE, + DEFAULT_EC + ) + + // there is nothing in the cmc, so onEncrypt will get active branch key + // material from the keystore. This makes 1 call to DDB and 1 to KMS + await testOnEncrypt(hkr, branchKeyIdA, encryptionMaterial) + expect(kmsSendSpy.callCount).equals(1) + expect(ddbSendSpy.callCount).equals(1) + }) + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#query-branch-keystore-onencrypt + //= type=test + //# The branch keystore persists [branch keys](#definitions) that are reused to derive unique data keys for envelope encryption to + //# reduce the number of calls to AWS KMS through the use of the + //# [cryptographic materials cache](../cryptographic-materials-cache.md). + //# OnEncrypt MUST call the Keystore's [GetActiveBranchKey](../branch-key-store.md#getactivebranchkey) operation with the following inputs: + //# - the `branchKeyId` used in this operation + //# If the Keystore's GetActiveBranchKey operation succeeds + //# the keyring MUST put the returned branch key materials in the cache using the + //# formula defined in [Appendix A](#appendix-a-cache-entry-identifier-formulas). + //# Otherwise, OnEncrypt MUST fail. + it('Material X already in CMC, request for Material X', async () => { + const hkr = new KmsHierarchicalKeyRingNode({ + branchKeyId: branchKeyIdA, + keyStore, + cacheLimitTtl, + }) + const encryptionMaterial = new NodeEncryptionMaterial( + TEST_ESDK_ALG_SUITE, + DEFAULT_EC + ) + + // in this call, onEncrypt queries the clients to get active branch + // key material and caches it in the cmc. + await testOnEncrypt(hkr, branchKeyIdA, encryptionMaterial) + expect(kmsSendSpy.callCount).equals(1) + expect(ddbSendSpy.callCount).equals(1) + + // in this call, onEncrypt needs the same active branch key material and they + // are already in the CMC + await testOnEncrypt(hkr, branchKeyIdA, encryptionMaterial) + expect(kmsSendSpy.callCount).equals(1) + expect(ddbSendSpy.callCount).equals(1) + }) + + it('Material A already in the CMC, ask for material B in keystore', async () => { + const hkr = new KmsHierarchicalKeyRingNode({ + branchKeyIdSupplier, + keyStore, + cacheLimitTtl, + }) + const encryptionMaterialA = new NodeEncryptionMaterial( + TEST_ESDK_ALG_SUITE, + EC_A + ) + const encryptionMaterialB = new NodeEncryptionMaterial( + TEST_ESDK_ALG_SUITE, + EC_B + ) + + // this call will get active branch key material A from the keystore and + // cache it + await testOnEncrypt(hkr, branchKeyIdA, encryptionMaterialA) + expect(kmsSendSpy.callCount).equals(1) + expect(ddbSendSpy.callCount).equals(1) + + // this call needs active branch key material B. It is not in the keystore + // so it will make network calls + await testOnEncrypt(hkr, branchKeyIdB, encryptionMaterialB) + expect(kmsSendSpy.callCount).equals(2) + expect(ddbSendSpy.callCount).equals(2) + }) + + it('CMC evictions occur due to long network calls', async () => { + const cacheLimitTtl = 10 / 1000 // set to 10 ms + const hkr = new KmsHierarchicalKeyRingNode({ + branchKeyId: branchKeyIdA, + keyStore, + cacheLimitTtl: cacheLimitTtl, + }) + const encryptionMaterial = new NodeEncryptionMaterial( + TEST_ESDK_ALG_SUITE, + DEFAULT_EC + ) + + // active branch key material is not cached, so make network calls and + // cache it + await testOnEncrypt(hkr, branchKeyIdA, encryptionMaterial) + expect(kmsSendSpy.callCount).equals(1) + expect(ddbSendSpy.callCount).equals(1) + + // stall for twice ttl such that the CMC is fully evicted + clock.tick(cacheLimitTtl * 2 * 1000) + + // active branch key material is not cached, so make network calls again + await testOnEncrypt(hkr, branchKeyIdA, encryptionMaterial) + expect(kmsSendSpy.callCount).equals(2) + expect(ddbSendSpy.callCount).equals(2) + }) + + it('CMC evictions occur due to capacity', async () => { + const maxCacheSize = 1 + const hkr = new KmsHierarchicalKeyRingNode({ + branchKeyIdSupplier, + keyStore, + cacheLimitTtl, + maxCacheSize, + }) + const encryptionMaterialA = new NodeEncryptionMaterial( + TEST_ESDK_ALG_SUITE, + EC_A + ) + const encryptionMaterialB = new NodeEncryptionMaterial( + TEST_ESDK_ALG_SUITE, + EC_B + ) + + // active branch key material A is not cached, so make network calls and + // cache it + await testOnEncrypt(hkr, branchKeyIdA, encryptionMaterialA) + expect(kmsSendSpy.callCount).equals(1) + expect(ddbSendSpy.callCount).equals(1) + + // active branch key material B is not cached, so make network calls and + // cache it. This evicts active branch key material A due to capacity + // being exceeded + await testOnEncrypt(hkr, branchKeyIdB, encryptionMaterialB) + expect(kmsSendSpy.callCount).equals(2) + expect(ddbSendSpy.callCount).equals(2) + + // active branch key material A is not cached, so make network calls and + // cache it again + await testOnEncrypt(hkr, branchKeyIdA, encryptionMaterialA) + expect(kmsSendSpy.callCount).equals(3) + expect(ddbSendSpy.callCount).equals(3) + }) + }) +}) diff --git a/modules/kms-keyring-node/test/kms_hkeyring_node.test.ts b/modules/kms-keyring-node/test/kms_hkeyring_node.test.ts new file mode 100644 index 000000000..37daaa0e0 --- /dev/null +++ b/modules/kms-keyring-node/test/kms_hkeyring_node.test.ts @@ -0,0 +1,397 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + AwsEsdkKeyObject, + EncryptedDataKey, + EncryptionContext, + KeyringTraceFlag, + NodeBranchKeyMaterial, + NodeDecryptionMaterial, + NodeEncryptionMaterial, + unwrapDataKey, +} from '@aws-crypto/material-management' +import { + IKmsHierarchicalKeyRingNode, + KmsHierarchicalKeyRingNode, +} from '../src/kms_hkeyring_node' +import chai, { expect } from 'chai' +import { + BRANCH_KEY, + BRANCH_KEY_ID, + BRANCH_KEY_ID_A, + BRANCH_KEY_ID_B, + CASE_A, + CASE_B, + DEFAULT_EC, + EC_A, + EC_B, + KEYSTORE, + TEST_ESDK_ALG_SUITE, + TTL, +} from './fixtures' +import { + CIPHERTEXT_STRUCTURE, + DECRYPT_FLAGS, + ENCRYPT_FLAGS, + PROVIDER_ID_HIERARCHY, +} from '../src/constants' +import { BranchKeyIdSupplier } from '@aws-crypto/kms-keyring' +import chaiAsPromised from 'chai-as-promised' +chai.use(chaiAsPromised) + +export class DummyBranchKeyIdSupplier implements BranchKeyIdSupplier { + private _cases: { [key: string]: string } = { + [CASE_A]: BRANCH_KEY_ID_A, + [CASE_B]: BRANCH_KEY_ID_B, + } + + getBranchKeyId(encryptionContext: EncryptionContext): string { + if (BRANCH_KEY in encryptionContext) { + const c = encryptionContext[BRANCH_KEY] + if (c in this._cases) { + return this._cases[c] + } + } + + throw new Error("Can't determine branchKeyId from context") + } +} +export const BRANCH_KEY_ID_SUPPLIER = new DummyBranchKeyIdSupplier() + +// a function to deep copy branch key material. This is needed so that the mock +// key store can return new branch key material every time +export function deepCopyBranchKeyMaterial(material: NodeBranchKeyMaterial) { + const branchKey = Buffer.from(material.branchKey()) + const branchKeyVersionAsString = material.branchKeyVersion.toString('utf-8') + const encryptionContext = { ...material.encryptionContext } + const branchKeyIdentifier = material.branchKeyIdentifier + return new NodeBranchKeyMaterial( + branchKey, + branchKeyIdentifier, + branchKeyVersionAsString, + encryptionContext + ) +} + +// a util function to test onEncrypt and expect an error while ensuring the +// encryption material is not modified +export async function testOnEncryptError( + hkr: IKmsHierarchicalKeyRingNode, + encryptionMaterial: NodeEncryptionMaterial, + errorMessage: string +) { + const expectedNumberOfEdks = encryptionMaterial.encryptedDataKeys.length + const expectedNumberOfTraces = encryptionMaterial.keyringTrace.length + const alreadyHasPdk = encryptionMaterial.hasUnencryptedDataKey + + await expect(hkr.onEncrypt(encryptionMaterial)).to.be.rejectedWith( + errorMessage + ) + + expect(encryptionMaterial.encryptedDataKeys).to.have.lengthOf( + expectedNumberOfEdks + ) + expect(encryptionMaterial.keyringTrace).to.have.lengthOf( + expectedNumberOfTraces + ) + expect(encryptionMaterial.hasUnencryptedDataKey).to.equal(alreadyHasPdk) +} + +// a util test function to run onEncrypt. It also makes sure that the correct +// modifications are made to the encryption material whether we are generating an pdk +// and wrapping it into an edk, OR just wrapping an existing pdk into a new edk +export async function testOnEncrypt( + hkr: IKmsHierarchicalKeyRingNode, + wrappingKeyName: string, + encryptionMaterial: NodeEncryptionMaterial +) { + // expect 1 more edk to be generated + const expectedNumberOfEdks = encryptionMaterial.encryptedDataKeys.length + 1 + // we expect one more trace to be added from the new edk. If there is also no + // pdk, there will be an extra generation trace for this + const expectedNumberOfTraces = + encryptionMaterial.keyringTrace.length + + (encryptionMaterial.hasUnencryptedDataKey ? 1 : 2) + + const alreadyHasPdk = encryptionMaterial.hasUnencryptedDataKey + let initialPdk: Uint8Array | AwsEsdkKeyObject | undefined = undefined + if (alreadyHasPdk) { + initialPdk = encryptionMaterial.getUnencryptedDataKey() + } + + await hkr.onEncrypt(encryptionMaterial) + + expect(encryptionMaterial.encryptedDataKeys).to.have.lengthOf( + expectedNumberOfEdks + ) + + // whether or not a pdk was generated or not, there should be a pdk + expect(encryptionMaterial.hasUnencryptedDataKey).to.be.true + if (alreadyHasPdk) { + const encryptedPdk = encryptionMaterial.getUnencryptedDataKey() + expect(encryptedPdk).to.equal(initialPdk) + } else { + // the pdk should have a length conforming to the key length specified by the + // algorithm suite + const encryptedPdk = unwrapDataKey( + encryptionMaterial.getUnencryptedDataKey() + ) + expect(encryptedPdk).to.have.lengthOf( + encryptionMaterial.suite.keyLengthBytes + ) + } + + expect(encryptionMaterial.keyringTrace).to.have.lengthOf( + expectedNumberOfTraces + ) + + // the edk created from this onEncrypt call is the most recent one. Thus, it + // will be the last edk + const lastEdk = encryptionMaterial.encryptedDataKeys[expectedNumberOfEdks - 1] + const { + providerId: edkProviderId, + providerInfo: edkProviderInfo, + encryptedDataKey: edkCiphertext, + } = lastEdk + const edkExpectedLength = + CIPHERTEXT_STRUCTURE.saltLength + + CIPHERTEXT_STRUCTURE.ivLength + + CIPHERTEXT_STRUCTURE.branchKeyVersionCompressedLength + + CIPHERTEXT_STRUCTURE.authTagLength + + encryptionMaterial.suite.keyLengthBytes + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#onencrypt + //= type=test + //# Otherwise, OnEncrypt MUST append a new [encrypted data key](../structures.md#encrypted-data-key) + //# to the encrypted data key list in the [encryption materials](../structures.md#encryption-materials), constructed as follows: + //# - [ciphertext](../structures.md#ciphertext): MUST be serialized as the [hierarchical keyring ciphertext](#ciphertext) + //# - [key provider id](../structures.md#key-provider-id): MUST be UTF8 Encoded "aws-kms-hierarchy" + //# - [key provider info](../structures.md#key-provider-information): MUST be the UTF8 Encoded AWS DDB response `branch-key-id` + expect(edkProviderId).to.equal(PROVIDER_ID_HIERARCHY) + expect(edkProviderInfo).to.equal(wrappingKeyName) + expect(edkCiphertext).to.have.lengthOf(edkExpectedLength) + + // if this is the first onEncrypt of the encryption material's lifetime, + // traces will look like [generate, encrypt]. Otherwise, it will look like + // [generate, encrypt, encrypt, ...] + expect(expectedNumberOfTraces).to.be.greaterThanOrEqual(2) + + const encryptTrace = + encryptionMaterial.keyringTrace[expectedNumberOfTraces - 1] + const generateTrace = encryptionMaterial.keyringTrace[0] + + expect(generateTrace.flags).to.equal( + KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY + ) + expect(generateTrace.keyNamespace).to.equal(PROVIDER_ID_HIERARCHY) + expect(generateTrace.keyName).to.equal(wrappingKeyName) + + expect(encryptTrace.flags).to.equal(ENCRYPT_FLAGS) + expect(encryptTrace.keyNamespace).to.equal(PROVIDER_ID_HIERARCHY) + expect(encryptTrace.keyName).to.equal(wrappingKeyName) +} + +// a util function to test onDecrypt and expect an error while ensuring the +// decryption material is not modified +export async function testOnDecryptError( + hkr: IKmsHierarchicalKeyRingNode, + edks: EncryptedDataKey[], + decryptionMaterial: NodeDecryptionMaterial, + errorMessage?: string, + errorMessages?: string[] +) { + const expectedNumberOfTraces = decryptionMaterial.keyringTrace.length + const alreadyHasPdk = decryptionMaterial.hasUnencryptedDataKey + + if (errorMessage) { + await expect(hkr.onDecrypt(decryptionMaterial, edks)).to.be.rejectedWith( + errorMessage as string + ) + } else { + try { + await hkr.onDecrypt(decryptionMaterial, edks) + } catch (error) { + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#ondecrypt + //= 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 + //# and MUST NOT modify the [decryption materials](structures.md#decryption-materials). + const errMsg = (error as Error).message + for (const expectedError of errorMessages as string[]) { + expect(errMsg.includes(expectedError)).to.be.true + } + } + } + + expect(decryptionMaterial.keyringTrace).to.have.lengthOf( + expectedNumberOfTraces + ) + expect(decryptionMaterial.hasUnencryptedDataKey).to.equal(alreadyHasPdk) +} + +// a util function that runs onDecrypt. This function ensures that the +// decryption material is accurately modified +export async function testOnDecrypt( + hkr: IKmsHierarchicalKeyRingNode, + expectedEncryptedPdk: Uint8Array, + edks: EncryptedDataKey[], + wrappingKeyName: string, + decryptionMaterial: NodeDecryptionMaterial +) { + // onDecrypt will add exactly 1 extra decrypt trace flag + const expectedNumberOfTraces = decryptionMaterial.keyringTrace.length + 1 + + await hkr.onDecrypt(decryptionMaterial, edks) + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#ondecrypt + //= type=test + //# If a decryption succeeds, this keyring MUST + //# add the resulting plaintext data key to the decryption materials and return the modified materials. + // if onDecrypt is successful, it should always have the pdk + expect(decryptionMaterial.hasUnencryptedDataKey).equals(true) + const decryptedPdk = unwrapDataKey(decryptionMaterial.getUnencryptedDataKey()) + // this pdk that was unwrapped during onDecrypt should be the expected pdk + expect(expectedEncryptedPdk).to.deep.equal(decryptedPdk) + + expect(decryptionMaterial.keyringTrace).to.have.lengthOf( + expectedNumberOfTraces + ) + + // the trace left by this onDecrypt call should be a decrypt flag + const decryptTrace = + decryptionMaterial.keyringTrace[expectedNumberOfTraces - 1] + expect(decryptTrace.keyNamespace).to.equal(PROVIDER_ID_HIERARCHY) + expect(decryptTrace.keyName).to.equal(wrappingKeyName) + expect(decryptTrace.flags).to.equal(DECRYPT_FLAGS) +} + +// this util function runs a roundtrip test with the provided encryption and +// decryption material, acting as a small CMM +export async function testRoundtrip( + hkr: IKmsHierarchicalKeyRingNode, + wrappingKeyName: string, + encryptionMaterial: NodeEncryptionMaterial = new NodeEncryptionMaterial( + TEST_ESDK_ALG_SUITE, + DEFAULT_EC + ), + decryptionMaterial: NodeDecryptionMaterial = new NodeDecryptionMaterial( + encryptionMaterial.suite, + encryptionMaterial.encryptionContext + ) +) { + // run onEncrypt with verification + await testOnEncrypt(hkr, wrappingKeyName, encryptionMaterial) + + // get the pdk and edks + const encryptedPdk = unwrapDataKey(encryptionMaterial.getUnencryptedDataKey()) + const edks = encryptionMaterial.encryptedDataKeys + + // try to decrypt the edks and expect to obtain the pdk from the encryption + // material + await testOnDecrypt( + hkr, + encryptedPdk, + edks, + wrappingKeyName, + decryptionMaterial + ) +} + +describe('KmsHierarchicalKeyRingNode: MPL tests', () => { + it('Test Hierarchy Client ESDK Suite', async () => { + const branchKeyId = BRANCH_KEY_ID + const keyStore = KEYSTORE + const cacheLimitTtl = TTL + const hkr = new KmsHierarchicalKeyRingNode({ + branchKeyId, + keyStore, + cacheLimitTtl, + }) + let encryptionMaterial = new NodeEncryptionMaterial( + TEST_ESDK_ALG_SUITE, + DEFAULT_EC + ) + + await testRoundtrip(hkr, branchKeyId, encryptionMaterial) + + // test with an initial pdk already existing + const initialPdk = new Uint8Array( + unwrapDataKey(encryptionMaterial.getUnencryptedDataKey()) + ) + encryptionMaterial = new NodeEncryptionMaterial( + TEST_ESDK_ALG_SUITE, + DEFAULT_EC + ).setUnencryptedDataKey(initialPdk, { + keyName: branchKeyId, + keyNamespace: PROVIDER_ID_HIERARCHY, + flags: KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY, + }) + + await testRoundtrip(hkr, branchKeyId, encryptionMaterial) + }) + + it('Test branch key id supplier', async () => { + const branchKeyIdSupplier = BRANCH_KEY_ID_SUPPLIER + const keyStore = KEYSTORE + const cacheLimitTtl = TTL + const hkr = new KmsHierarchicalKeyRingNode({ + branchKeyIdSupplier, + keyStore, + cacheLimitTtl, + }) + const encryptionMaterialA = new NodeEncryptionMaterial( + TEST_ESDK_ALG_SUITE, + EC_A + ) + const encryptionMaterialB = new NodeEncryptionMaterial( + TEST_ESDK_ALG_SUITE, + EC_B + ) + + await testRoundtrip(hkr, BRANCH_KEY_ID_A, encryptionMaterialA) + await testRoundtrip(hkr, BRANCH_KEY_ID_B, encryptionMaterialB) + }) + + it('Test invalid data key error', async () => { + const branchKeyIdSupplier = BRANCH_KEY_ID_SUPPLIER + const keyStore = KEYSTORE + const cacheLimitTtl = TTL + const hkr = new KmsHierarchicalKeyRingNode({ + branchKeyIdSupplier, + keyStore, + cacheLimitTtl, + }) + const encryptionMaterial = new NodeEncryptionMaterial( + TEST_ESDK_ALG_SUITE, + EC_A + ) + const decyrptionMaterial = new NodeDecryptionMaterial( + TEST_ESDK_ALG_SUITE, + EC_B + ) + + // encrypt the generated pdk using branch key A as a wrapper + await testOnEncrypt(hkr, BRANCH_KEY_ID_A, encryptionMaterial) + + const encryptedPdk = unwrapDataKey( + encryptionMaterial.getUnencryptedDataKey() + ) + const edks = encryptionMaterial.encryptedDataKeys + + // now we want to decrypt the edk with branch key B. However, the edk given + // to onDecrypt knows that it was encrypted with branch key A. The edk + // doesn't even pass the filter to attempt decryption. + await expect( + testOnDecrypt( + hkr, + encryptedPdk, + edks, + BRANCH_KEY_ID_B, + decyrptionMaterial + ) + ).to.be.rejectedWith( + "There must be an encrypted data key that matches this keyring's configuration" + ) + }) +}) diff --git a/modules/kms-keyring-node/tsconfig.json b/modules/kms-keyring-node/tsconfig.json index f070d1189..f36bd25a3 100644 --- a/modules/kms-keyring-node/tsconfig.json +++ b/modules/kms-keyring-node/tsconfig.json @@ -8,6 +8,10 @@ "exclude": ["node_modules/**"], "references": [ { "path": "../material-management" }, - { "path": "../kms-keyring" } + { "path": "../kms-keyring" }, + { "path": "../branch-keystore-node" }, + { "path": "../kdf-ctr-mode-node" }, + { "path": "../serialize" }, + { "path": "../cache-material" } ] -} \ No newline at end of file +} diff --git a/modules/kms-keyring/src/branch_key_id_supplier.ts b/modules/kms-keyring/src/branch_key_id_supplier.ts new file mode 100644 index 000000000..6cec43aef --- /dev/null +++ b/modules/kms-keyring/src/branch_key_id_supplier.ts @@ -0,0 +1,23 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { EncryptionContext } from '@aws-crypto/material-management' + +//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#branch-key-supplier +//# The Branch Key Supplier is an interface containing the `GetBranchKeyId` operation. +//# This operation MUST take in an encryption context as input, +//# and return a branch key id (string) as output. +export interface BranchKeyIdSupplier { + getBranchKeyId(encryptionContext: EncryptionContext): string +} + +// type guard +export function isBranchKeyIdSupplier( + supplier: any +): supplier is BranchKeyIdSupplier { + return ( + typeof supplier === 'object' && + supplier !== null && + typeof supplier.getBranchKeyId === 'function' + ) +} diff --git a/modules/kms-keyring/src/index.ts b/modules/kms-keyring/src/index.ts index 50a4220e9..d2e698623 100644 --- a/modules/kms-keyring/src/index.ts +++ b/modules/kms-keyring/src/index.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 export * from './kms_client_supplier' +export { getRegionFromIdentifier, parseAwsKmsKeyArn } from './arn_parsing' export * from './kms_keyring' export * from './kms_mrk_keyring' export * from './kms_mrk_discovery_keyring' @@ -10,3 +11,4 @@ export * from './region_from_kms_key_arn' export * from './kms_mrk_strict_multi_keyring' export * from './kms_mrk_discovery_multi_keyring' export { AwsEsdkKMSInterface } from './kms_types' +export * from './branch_key_id_supplier' diff --git a/modules/kms-keyring/test/branch_key_id_supplier.test.ts b/modules/kms-keyring/test/branch_key_id_supplier.test.ts new file mode 100644 index 000000000..10f399c12 --- /dev/null +++ b/modules/kms-keyring/test/branch_key_id_supplier.test.ts @@ -0,0 +1,29 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { EncryptionContext } from '@aws-crypto/material-management' +import { BranchKeyIdSupplier, isBranchKeyIdSupplier } from '../src' +import { expect } from 'chai' + +describe('Branch key id supplier', () => { + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#branch-key-supplier + //= type=test + //# The Branch Key Supplier is an interface containing the GetBranchKeyId + //# operation. This operation MUST take in an encryption context as input, and + //# return a branch key id (string) as output. + it('Can implement the interface', () => { + class Example implements BranchKeyIdSupplier { + getBranchKeyId(encryptionContext: EncryptionContext): string { + return '' in encryptionContext ? '' : '' + } + } + + expect(new Example().getBranchKeyId({})).to.equal('') + }) + + it('Type guard', () => { + expect(isBranchKeyIdSupplier(undefined as any)).to.be.false + expect(isBranchKeyIdSupplier(null as any)).to.be.false + expect(isBranchKeyIdSupplier({} as any)).to.be.false + }) +}) diff --git a/modules/material-management/package.json b/modules/material-management/package.json index b53652af1..39d5087a4 100644 --- a/modules/material-management/package.json +++ b/modules/material-management/package.json @@ -20,7 +20,8 @@ "dependencies": { "asn1.js": "^5.3.0", "bn.js": "^5.1.1", - "tslib": "^2.2.0" + "tslib": "^2.2.0", + "util": "^0.12.5" }, "sideEffects": false, "main": "./build/main/src/index.js", diff --git a/modules/material-management/src/cryptographic_material.ts b/modules/material-management/src/cryptographic_material.ts index 554b1ecac..a5fc25cd2 100644 --- a/modules/material-management/src/cryptographic_material.ts +++ b/modules/material-management/src/cryptographic_material.ts @@ -17,6 +17,7 @@ import { KeyringTrace, KeyringTraceFlag } from './keyring_trace' import { NodeAlgorithmSuite } from './node_algorithms' import { WebCryptoAlgorithmSuite } from './web_crypto_algorithms' import { needs } from './needs' +import { validate, version } from 'uuid' /* KeyObject were introduced in v11. * They protect the data key better than a Buffer. @@ -115,6 +116,106 @@ export interface CryptographicMaterial> { encryptionContext: Readonly } +//= aws-encryption-sdk-specification/framework/structures.md#structure-3 +//# This structure MUST include all of the following fields: +//# +//# - [Branch Key](#branch-key) +//# - [Branch Key Id](#branch-key-id) +//# - [Branch Key Version](#branch-key-version) +//# - [Encryption Context](#encryption-context-3) +// structure is based on https://github.com/aws/aws-cryptographic-material-providers-library/blob/main/AwsCryptographicMaterialProviders/dafny/AwsCryptographyKeyStore/Model/KeyStore.smithy#L323 +export interface BranchKeyMaterial { + branchKey(): Readonly + branchKeyIdentifier: string + branchKeyVersion: Readonly + encryptionContext: Readonly +} + +export class NodeBranchKeyMaterial implements BranchKeyMaterial { + // all attributes are readonly so they are accessible outside the class but + // they cannot be modified + + // since all fields are objects, keep them immutable from external changes via + // shared memory + + // we want the branch key to be mutable within the class but immutable outside + // the class + private _branchKey: Buffer + declare readonly branchKeyIdentifier: string + declare readonly branchKeyVersion: Readonly + declare readonly encryptionContext: Readonly + + constructor( + branchKey: Buffer, + branchKeyIdentifier: string, + branchKeyVersion: string, + encryptionContext: EncryptionContext + ) { + /* Precondition: Branch key must be a Buffer */ + needs(branchKey instanceof Buffer, 'Branch key must be a Buffer') + + /* Precondition: Branch key id must be a string */ + needs( + typeof branchKeyIdentifier === 'string', + 'Branch key id must be a string' + ) + + /* Precondition: Branch key version must be a string */ + needs( + typeof branchKeyVersion === 'string', + 'Branch key version must be a string' + ) + + /* Precondition: encryptionContext must be an object, even if it is empty */ + needs( + encryptionContext && typeof encryptionContext === 'object', + 'Encryption context must be an object' + ) + + /* Precondition: branchKey must be a 32 byte-long buffer */ + needs(branchKey.length === 32, 'Branch key must be 32 bytes long') + + /* Precondition: branch key ID is required */ + needs(branchKeyIdentifier, 'Empty branch key ID') + + /* Precondition: branch key version must be valid version 4 uuid */ + needs( + validate(branchKeyVersion) && version(branchKeyVersion) === 4, + 'Branch key version must be valid version 4 uuid' + ) + + /* Postcondition: branch key is immutable */ + this._branchKey = Buffer.from(branchKey) + + /* Postconditon: encryption context is immutable */ + this.encryptionContext = Object.freeze({ ...encryptionContext }) + + this.branchKeyIdentifier = branchKeyIdentifier + + this.branchKeyVersion = Buffer.from(branchKeyVersion, 'utf-8') + + Object.setPrototypeOf(this, NodeBranchKeyMaterial.prototype) + /* Postcondition: instance is frozen */ + // preventing any modifications to its properties or methods. + Object.freeze(this) + } + + // makes the branch key public to users wrapped in immutable access + branchKey(): Readonly { + return this._branchKey + } + + // this capability is not required of branch key materials according to the + // specification. Using this is a good security practice so that the data + // key's bytes are not preserved even in free memory + zeroUnencryptedDataKey(): BranchKeyMaterial { + this._branchKey.fill(0) + return this + } +} +// make the class immutable +frozenClass(NodeBranchKeyMaterial) + export interface EncryptionMaterial> extends CryptographicMaterial { encryptedDataKeys: EncryptedDataKey[] @@ -372,6 +473,10 @@ export function isDecryptionMaterial( ) } +export function isBranchKeyMaterial(obj: any): obj is NodeBranchKeyMaterial { + return obj instanceof NodeBranchKeyMaterial +} + export function decorateCryptographicMaterial< T extends CryptographicMaterial >(material: T, setFlag: KeyringTraceFlag) { diff --git a/modules/material-management/src/index.ts b/modules/material-management/src/index.ts index bce27afdd..667a30997 100644 --- a/modules/material-management/src/index.ts +++ b/modules/material-management/src/index.ts @@ -41,6 +41,7 @@ export * from './materials_manager' export { NodeEncryptionMaterial, NodeDecryptionMaterial, + NodeBranchKeyMaterial, } from './cryptographic_material' export { isValidCryptoKey, @@ -55,6 +56,7 @@ export { export { isEncryptionMaterial, isDecryptionMaterial, + isBranchKeyMaterial, } from './cryptographic_material' export { unwrapDataKey, diff --git a/modules/material-management/src/types.ts b/modules/material-management/src/types.ts index 0b1646cce..7548ae536 100644 --- a/modules/material-management/src/types.ts +++ b/modules/material-management/src/types.ts @@ -5,6 +5,7 @@ import { NodeAlgorithmSuite } from './node_algorithms' import { WebCryptoAlgorithmSuite } from './web_crypto_algorithms' import { EncryptedDataKey } from './encrypted_data_key' import { + NodeBranchKeyMaterial, NodeDecryptionMaterial, NodeEncryptionMaterial, WebCryptoDecryptionMaterial, @@ -79,6 +80,8 @@ export type DecryptionMaterial = Suite extends NodeAlgorithmSuite ? WebCryptoDecryptionMaterial : never +export type BranchKeyMaterial = NodeBranchKeyMaterial + /* These are copies of the v12 Node.js types. * I copied them here to avoid exporting v12 types * and forcing consumers to install/use v12 in their projects. diff --git a/modules/material-management/test/cryptographic_material.test.ts b/modules/material-management/test/cryptographic_material.test.ts index b0e7e110a..bfce00aef 100644 --- a/modules/material-management/test/cryptographic_material.test.ts +++ b/modules/material-management/test/cryptographic_material.test.ts @@ -29,8 +29,9 @@ import { unwrapDataKey, wrapWithKeyObjectIfSupported, supportsKeyObject, + NodeBranchKeyMaterial, } from '../src/cryptographic_material' - +import { v1, v4, v3, v5 } from 'uuid' import { createSecretKey } from 'crypto' describe('decorateCryptographicMaterial', () => { @@ -990,6 +991,191 @@ describe('decorateWebCryptoMaterial:Helpers', () => { }) }) +describe('NodeBranchKeyMaterial', () => { + const branchKey = Buffer.alloc(32) + const branchKeyId = 'id' + const branchKeyVersion = v4() + const encryptionContext = {} + const test = new NodeBranchKeyMaterial( + branchKey, + branchKeyId, + branchKeyVersion, + encryptionContext + ) + + it('Precondition: Branch key must be a Buffer', () => { + for (const branchKey of [null, undefined, {}, 0, '']) { + expect( + () => + new NodeBranchKeyMaterial( + branchKey as any, + branchKeyId, + branchKeyVersion, + encryptionContext + ) + ).to.throw('Branch key must be a Buffer') + } + }) + + it('Precondition: Branch key id must be a string', () => { + for (const branchKeyId of [null, undefined, {}, 0]) { + expect( + () => + new NodeBranchKeyMaterial( + branchKey, + branchKeyId as any, + branchKeyVersion, + encryptionContext + ) + ).to.throw('Branch key id must be a string') + } + }) + + it('Precondition: Branch key version must be a string', () => { + for (const branchKeyVersion of [null, undefined, {}, 0]) { + expect( + () => + new NodeBranchKeyMaterial( + branchKey, + branchKeyId, + branchKeyVersion as any, + encryptionContext + ) + ).to.throw('Branch key version must be a string') + } + }) + + it('Precondition: encryptionContext must be an object, even if it is empty', () => { + for (const encryptionContext of [null, undefined, 0, '']) { + expect( + () => + new NodeBranchKeyMaterial( + branchKey, + branchKeyId, + branchKeyVersion, + encryptionContext as any + ) + ).to.throw('Encryption context must be an object') + } + }) + + it('Precondition: branchKey must be a 32 byte-long buffer', () => { + expect( + () => + new NodeBranchKeyMaterial( + Buffer.alloc(10), + branchKeyId, + branchKeyVersion, + encryptionContext + ) + ).to.throw('Branch key must be 32 bytes long') + }) + + it('Precondition: branch key ID is required', () => { + expect( + () => + new NodeBranchKeyMaterial( + branchKey, + '', + branchKeyVersion, + encryptionContext + ) + ).to.throw('Empty branch key ID') + }) + + it('Precondition: branch key version must be valid version 4 uuid', () => { + expect( + () => + new NodeBranchKeyMaterial(branchKey, branchKeyId, '', encryptionContext) + ).to.throw('Branch key version must be valid version 4 uuid') + + expect( + () => + new NodeBranchKeyMaterial( + branchKey, + branchKeyId, + v1(), + encryptionContext + ) + ).to.throw('Branch key version must be valid version 4 uuid') + + const namespace = v4() + const name = 'example.com' + expect( + () => + new NodeBranchKeyMaterial( + branchKey, + branchKeyId, + v3(name, namespace), + encryptionContext + ) + ).to.throw('Branch key version must be valid version 4 uuid') + expect( + () => + new NodeBranchKeyMaterial( + branchKey, + branchKeyId, + v5(name, namespace), + encryptionContext + ) + ).to.throw('Branch key version must be valid version 4 uuid') + + expect(() => { + new NodeBranchKeyMaterial( + branchKey, + branchKeyId, + v4().toUpperCase(), + encryptionContext + ) + }) + }) + + it('Postcondition: branch key is immutable', () => { + // attempt to modify the buffer used to initialize branchKey of the + // materials. The materials' branch key should remain unaffected because + // it's a deep copy + branchKey[0] = 0xff // some non-zero value + // the branch key was initialized to be a completely zeroed out buffer. + // Let's ensure it's still zeroed out + expect(test.branchKey()[0]).to.equal(0x00) + }) + + it('Postconditon: encryption context is immutable', () => { + expect(Object.isFrozen(test.encryptionContext)).to.be.true + }) + + it('Postcondition: instance is frozen', () => { + expect(Object.isFrozen(test)).to.equal(true) + }) + + it('Class is frozen', () => { + expect(Object.isFrozen(NodeBranchKeyMaterial)).to.equal(true) + expect(Object.isFrozen(NodeBranchKeyMaterial.prototype)).to.equal(true) + }) + + //= aws-encryption-sdk-specification/framework/structures.md#structure-3 + //= type=test + //# This structure MUST include all of the following fields: + //# + //# - [Branch Key](#branch-key) + //# - [Branch Key Id](#branch-key-id) + //# - [Branch Key Version](#branch-key-version) + //# - [Encryption Context](#encryption-context-3) + it('All attributes are initialized properly', () => { + expect(test.branchKey()).to.deep.equals(Buffer.alloc(32)) + expect(test.branchKeyIdentifier).to.equal(branchKeyId) + expect(test.branchKeyVersion.toString('utf-8')).to.equal(branchKeyVersion) + expect(Object.keys(test.encryptionContext).length === 0).to.equal( + Object.keys(encryptionContext).length === 0 + ) + }) + + it('Zero the branch key', () => { + const zeroedMaterial = test.zeroUnencryptedDataKey() + expect(zeroedMaterial.branchKey().every((byte) => byte === 0)).to.be.true + }) +}) + describe('NodeEncryptionMaterial', () => { const suite = new NodeAlgorithmSuite( AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16 diff --git a/modules/serialize/package.json b/modules/serialize/package.json index 5093618bb..10139a283 100644 --- a/modules/serialize/package.json +++ b/modules/serialize/package.json @@ -20,7 +20,8 @@ "@aws-crypto/material-management": "file:../material-management", "asn1.js": "^5.3.0", "bn.js": "^5.1.1", - "tslib": "^2.2.0" + "tslib": "^2.2.0", + "uuid": "^10.0.0" }, "sideEffects": false, "main": "./build/main/src/index.js", diff --git a/modules/serialize/src/index.ts b/modules/serialize/src/index.ts index eca7edfc3..6760836c5 100644 --- a/modules/serialize/src/index.ts +++ b/modules/serialize/src/index.ts @@ -12,3 +12,4 @@ export * from './identifiers' export * from './uint_util' export * from './signature_info' export * from './ecdsa_signature' +export * from './uuidv4_factory' diff --git a/modules/serialize/src/uuidv4_factory.ts b/modules/serialize/src/uuidv4_factory.ts new file mode 100644 index 000000000..4655f8e4c --- /dev/null +++ b/modules/serialize/src/uuidv4_factory.ts @@ -0,0 +1,68 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { needs } from '@aws-crypto/material-management' +import { validate, version } from 'uuid' + +// function to validate a string as uuidv4 +const validateUuidv4 = (input: string): boolean => + validate(input) && version(input) === 4 + +// accepts user defined lambda functions to convert between a string and +// compressed hex encoded +// bytes. This factory is a higher order function that returns the compression +// and decompression functions based on the input lambda functions +export function uuidv4Factory( + stringToHexBytes: (input: string) => Uint8Array, + hexBytesToString: (input: Uint8Array) => string +) { + return { uuidv4ToCompressedBytes, decompressBytesToUuidv4 } + + // remove the '-' chars from the uuid string and convert to hex bytes + function uuidv4ToCompressedBytes(uuidString: string): Uint8Array { + /* Precondition: Input string must be valid uuidv4 */ + needs(validateUuidv4(uuidString), 'Input must be valid uuidv4') + + const uuidBytes = new Uint8Array( + stringToHexBytes(uuidString.replace(/-/g, '')) + ) + + /* Postcondition: Compressed bytes must have correct byte length */ + needs( + uuidBytes.length === 16, + 'Unable to convert uuid into compressed bytes' + ) + + return uuidBytes + } + + // convert the hex bytes to a string. Reconstruct the uuidv4 string with the + // '-' chars + function decompressBytesToUuidv4(uuidBytes: Uint8Array): string { + /* Precondition: Compressed bytes must have correct byte length */ + needs(uuidBytes.length === 16, 'Compressed uuid has incorrect byte length') + + const hex = hexBytesToString(uuidBytes) + let uuidString: string + + try { + // These represent the ranges of the uuidv4 string that contain + // alphanumeric chars. We want to rebuild the proper uuidv4 string by + // joining these segments with the '-' char + uuidString = [ + hex.slice(0, 8), + hex.slice(8, 12), + hex.slice(12, 16), + hex.slice(16, 20), + hex.slice(20), + ].join('-') + } catch { + throw new Error('Unable to decompress UUID compressed bytes') + } + + /* Postcondition: Output string must be valid uuidv4 */ + needs(validateUuidv4(uuidString), 'Input must represent a uuidv4') + + return uuidString + } +} diff --git a/modules/serialize/test/uuidv4_factory.test.ts b/modules/serialize/test/uuidv4_factory.test.ts new file mode 100644 index 000000000..d5cfd944c --- /dev/null +++ b/modules/serialize/test/uuidv4_factory.test.ts @@ -0,0 +1,114 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// tests contains MPL tests: https://github.com/aws/aws-cryptographic-material-providers-library/blob/da6812fa30315fda75d4277f814d1d0e36e22498/StandardLibrary/test/UUID.dfy + +import { v3, v5, v1, v4 } from 'uuid' +import { uuidv4Factory } from '../src/uuidv4_factory' +import { expect } from 'chai' + +const stringToHexBytes = (input: string): Uint8Array => + new Uint8Array(Buffer.from(input, 'hex')) + +const hexBytesToString = (input: Uint8Array): string => + Buffer.from(input).toString('hex') + +const { uuidv4ToCompressedBytes, decompressBytesToUuidv4 } = uuidv4Factory( + stringToHexBytes, + hexBytesToString +) + +const uuidString = '92382658-b7a0-4d97-9c49-cee4e672a3b3' +const byteUuid = new Uint8Array([ + 146, 56, 38, 88, 183, 160, 77, 151, 156, 73, 206, 228, 230, 114, 163, 179, +]) +const wrongByteUuid = new Uint8Array([ + 146, 56, 38, 88, 183, 160, 77, 151, 156, 73, 206, 228, 230, 114, 163, 178, +]) + +describe('uuidv4Factory', () => { + it('Test roundtrip string conversion', () => { + const stringToBytes = uuidv4ToCompressedBytes(uuidString) + expect(stringToBytes).has.lengthOf(16) + const bytesToString = decompressBytesToUuidv4(stringToBytes) + expect(bytesToString).to.equal(uuidString) + }) + + it('Test roundtrip byte conversion', () => { + const bytesToString = decompressBytesToUuidv4(byteUuid) + const stringToBytes = uuidv4ToCompressedBytes(bytesToString) + expect(stringToBytes).has.lengthOf(16) + expect(stringToBytes).to.deep.equal(byteUuid) + }) + + it('Test generate and conversion', () => { + const uuid = v4() + const uuidBytes = uuidv4ToCompressedBytes(uuid) + const bytesToString = decompressBytesToUuidv4(uuidBytes) + const stringToBytes = uuidv4ToCompressedBytes(bytesToString) + + expect(stringToBytes).has.lengthOf(16) + expect(stringToBytes).to.deep.equal(uuidBytes) + + const uuidStringToBytes = uuidv4ToCompressedBytes(uuid) + expect(uuidStringToBytes).has.lengthOf(16) + const uuidBytesToString = decompressBytesToUuidv4(uuidStringToBytes) + expect(uuidBytesToString).to.equal(uuid) + }) + + describe('decompressBytesToUuidv4', () => { + it('Precondition: Compressed bytes must have correct byte length', () => { + expect(() => decompressBytesToUuidv4(new Uint8Array([0]))).to.throw( + 'Compressed uuid has incorrect byte length' + ) + }) + + it('Postcondition: Output string must be valid uuidv4', () => { + expect(() => + decompressBytesToUuidv4(new Uint8Array(Buffer.alloc(16))) + ).to.throw('Input must represent a uuidv4') + }) + + it('Test success', () => { + const fromBytes = decompressBytesToUuidv4(byteUuid) + expect(fromBytes).to.equal(uuidString) + }) + + it('Test failure', () => { + const fromBytes = decompressBytesToUuidv4(wrongByteUuid) + expect(fromBytes).to.not.equals(uuidString) + }) + }) + + describe('uuidv4ToCompressedBytes', () => { + it('Precondition: Input string must be valid uuidv4', () => { + expect(() => uuidv4ToCompressedBytes(v1())).to.throw( + 'Input must be valid uuidv4' + ) + + const name = 'example.com' + const namespace = uuidString + expect(() => uuidv4ToCompressedBytes(v3(name, namespace))).to.throw( + 'Input must be valid uuidv4' + ) + + expect(() => uuidv4ToCompressedBytes(v5(name, namespace))).to.throw( + 'Input must be valid uuidv4' + ) + }) + + it('Postcondition: Compressed bytes must have correct byte length', () => { + expect(() => uuidv4ToCompressedBytes(uuidString)).to.not.throw() + }) + + it('Test success', () => { + const fromBytes = uuidv4ToCompressedBytes(uuidString) + expect(fromBytes).to.deep.equal(byteUuid) + }) + + it('Test failure', () => { + const fromBytes = uuidv4ToCompressedBytes(uuidString) + expect(fromBytes).to.not.deep.equals(wrongByteUuid) + }) + }) +}) diff --git a/package.json b/package.json index 7ca9df489..d8ae5dc49 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,8 @@ "author": "aws-crypto-tools-team@amazon.com", "license": "Apache-2.0", "dependencies": { + "@aws-crypto/branch-keystore-node": "file:modules/branch-keystore-node", + "@aws-crypto/kdf-ctr-mode-node": "file:modules/kdf-ctr-mode-node", "@aws-crypto/cache-material": "file:modules/cache-material", "@aws-crypto/caching-materials-manager-browser": "file:modules/caching-materials-manager-browser", "@aws-crypto/caching-materials-manager-node": "file:modules/caching-materials-manager-node", @@ -98,6 +100,7 @@ ], "devDependencies": { "@aws-sdk/credential-provider-node": "^3.362.0", + "@aws-sdk/karma-credential-loader": "^3.38.0", "@aws-sdk/util-base64": "^3.374.0", "@jsdevtools/coverage-istanbul-loader": "^3.0.5", "@types/bn.js": "^5.1.0", @@ -106,6 +109,7 @@ "@types/from2": "^2.3.0", "@types/mocha": "^9.0.0", "@types/node": "^16.9.1", + "@types/uuid": "^10.0.0", "@typescript-eslint/eslint-plugin": "^4.31.1", "@typescript-eslint/parser": "^4.31.1", "aws-sdk": "^2.1409.0", @@ -134,7 +138,7 @@ "rimraf": "^6.0.1", "source-map-support": "^0.5.19", "tslib": "^2.3.0", - "typescript": "^4.4.3", + "typescript": "^4.9.5", "verdaccio": "^5.13.1", "webpack": "^5.94.0", "webpack-cli": "^4.7.2" diff --git a/wallaby.conf.js b/wallaby.conf.js index 8ba950b94..ce8621cc3 100644 --- a/wallaby.conf.js +++ b/wallaby.conf.js @@ -2,39 +2,43 @@ // SPDX-License-Identifier: Apache-2.0 const compilerOptions = Object.assign({ - 'esModuleInterop': true, - 'target': 'esnext', - 'module': 'commonjs' + esModuleInterop: true, + target: 'esnext', + module: 'commonjs', }) module.exports = function (wallaby) { - var path = require('path'); - process.env.NODE_PATH += path.delimiter + path.join(wallaby.localProjectDir, 'core', 'node_modules'); + var path = require('path') + process.env.NODE_PATH += + path.delimiter + path.join(wallaby.localProjectDir, 'core', 'node_modules') return { files: [ 'modules/**/src/**/*.ts', 'modules/**/fixtures.ts', - { pattern: 'modules/**/test/**/*.test.ts', ignore: true}, - { pattern: 'modules/**/node_modules/**', ignore: true}, - { pattern: 'modules/**/build/**', ignore: true}, - { pattern: 'modules/*-browser/**/*.ts', ignore: true}, - { pattern: 'modules/*-backend/**/*.ts', ignore: true}, + { pattern: 'modules/**/test/**/*.test.ts', ignore: true }, + { pattern: 'modules/**/node_modules/**', ignore: true }, + { pattern: 'modules/**/build/**', ignore: true }, + { pattern: 'modules/*-browser/**/*.ts', ignore: true }, + { pattern: 'modules/*-backend/**/*.ts', ignore: true }, ], tests: [ 'modules/**/test/**/*test.ts', '!modules/**/node_modules/**', '!modules/**/build/**', - '!modules/*-+(browser|backend)/**/*.ts' - ], - filesWithNoCoverageCalculated: [ - 'modules/**/src/index.ts' + '!modules/*-+(browser|backend)/**/*.ts', ], + filesWithNoCoverageCalculated: ['modules/**/src/index.ts'], testFramework: 'mocha', compilers: { - '**/*.ts': wallaby.compilers.typeScript(compilerOptions) + '**/*.ts': wallaby.compilers.typeScript(compilerOptions), + }, + env: { + type: 'node', + params: { + env: 'AWS_REGION=us-west-2;AWS_CONTAINER_CREDENTIALS_FULL_URI=http://127.0.0.1:9911' + }, }, - env: { type: 'node' }, debug: true, } } From d9bc2ab2b2abd5b0a97cd165587064d59892be69 Mon Sep 17 00:00:00 2001 From: seebees Date: Fri, 13 Dec 2024 11:34:29 -0800 Subject: [PATCH 02/25] roolback unneeded changes --- modules/cache-material/package.json | 3 +- modules/kms-keyring-node/package.json | 1 - modules/material-management/package.json | 3 +- package-lock.json | 1044 ++++++++++++++++++---- package.json | 7 +- 5 files changed, 865 insertions(+), 193 deletions(-) diff --git a/modules/cache-material/package.json b/modules/cache-material/package.json index ec704e0ed..f141893e5 100644 --- a/modules/cache-material/package.json +++ b/modules/cache-material/package.json @@ -22,8 +22,7 @@ "@aws-crypto/serialize": "file:../serialize", "@types/lru-cache": "^5.1.0", "lru-cache": "^6.0.0", - "tslib": "^2.2.0", - "util": "^0.12.5" + "tslib": "^2.2.0" }, "sideEffects": false, "main": "./build/main/src/index.js", diff --git a/modules/kms-keyring-node/package.json b/modules/kms-keyring-node/package.json index 1cc62bac7..802b7d846 100644 --- a/modules/kms-keyring-node/package.json +++ b/modules/kms-keyring-node/package.json @@ -27,7 +27,6 @@ "@aws-crypto/serialize": "file:../serialize", "@aws-sdk/client-dynamodb": "^3.621.0", "@aws-sdk/client-kms": "^3.362.0", - "sinon": "^18.0.0", "tslib": "^2.2.0" }, "sideEffects": false, diff --git a/modules/material-management/package.json b/modules/material-management/package.json index 39d5087a4..b53652af1 100644 --- a/modules/material-management/package.json +++ b/modules/material-management/package.json @@ -20,8 +20,7 @@ "dependencies": { "asn1.js": "^5.3.0", "bn.js": "^5.1.1", - "tslib": "^2.2.0", - "util": "^0.12.5" + "tslib": "^2.2.0" }, "sideEffects": false, "main": "./build/main/src/index.js", diff --git a/package-lock.json b/package-lock.json index be9ea1e4c..2cc45ecd5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "modules/*" ], "dependencies": { + "@aws-crypto/branch-keystore-node": "file:modules/branch-keystore-node", "@aws-crypto/cache-material": "file:modules/cache-material", "@aws-crypto/caching-materials-manager-browser": "file:modules/caching-materials-manager-browser", "@aws-crypto/caching-materials-manager-node": "file:modules/caching-materials-manager-node", @@ -28,6 +29,7 @@ "@aws-crypto/integration-browser": "file:modules/integration-browser", "@aws-crypto/integration-node": "file:modules/integration-node", "@aws-crypto/integration-vectors": "file:modules/integration-vectors", + "@aws-crypto/kdf-ctr-mode-node": "file:modules/kdf-ctr-mode-node", "@aws-crypto/kms-keyring": "file:modules/kms-keyring", "@aws-crypto/kms-keyring-browser": "file:modules/kms-keyring-browser", "@aws-crypto/kms-keyring-node": "file:modules/kms-keyring-node", @@ -78,6 +80,7 @@ "nyc": "^15.1.0", "prettier": "^2.0.4", "rimraf": "^6.0.1", + "sion": "^0.0.1", "source-map-support": "^0.5.19", "tslib": "^2.3.0", "typescript": "^4.4.3", @@ -86,6 +89,16 @@ "webpack-cli": "^4.7.2" } }, + "modules/branch-keystore-node": { + "version": "4.0.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/kms-keyring": "file:../kms-keyring", + "@aws-sdk/client-dynamodb": "^3.616.0", + "@aws-sdk/util-dynamodb": "^3.616.0", + "tslib": "^2.2.0" + } + }, "modules/cache-material": { "name": "@aws-crypto/cache-material", "version": "4.0.1", @@ -164,9 +177,11 @@ "version": "4.0.2", "license": "Apache-2.0", "dependencies": { + "@aws-crypto/branch-keystore-node": "file:../branch-keystore-node", "@aws-crypto/caching-materials-manager-node": "file:../caching-materials-manager-node", "@aws-crypto/decrypt-node": "file:../decrypt-node", "@aws-crypto/encrypt-node": "file:../encrypt-node", + "@aws-crypto/kms-keyring": "file:../kms-keyring", "@aws-crypto/kms-keyring-node": "file:../kms-keyring-node", "@aws-crypto/material-management-node": "file:../material-management-node", "@aws-crypto/raw-aes-keyring-node": "file:../raw-aes-keyring-node", @@ -493,6 +508,16 @@ "yauzl": "^2.10.0" } }, + "modules/kdf-ctr-mode-node": { + "version": "4.0.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.2.0" + }, + "devDependencies": { + "@types/sinon": "^17.0.3" + } + }, "modules/kms-keyring": { "name": "@aws-crypto/kms-keyring", "version": "4.0.1", @@ -519,8 +544,13 @@ "version": "4.0.1", "license": "Apache-2.0", "dependencies": { + "@aws-crypto/branch-keystore-node": "file:../branch-keystore-node", + "@aws-crypto/cache-material": "file:../cache-material", + "@aws-crypto/kdf-ctr-mode-node": "file:../kdf-ctr-mode-node", "@aws-crypto/kms-keyring": "file:../kms-keyring", "@aws-crypto/material-management-node": "file:../material-management-node", + "@aws-crypto/serialize": "file:../serialize", + "@aws-sdk/client-dynamodb": "^3.621.0", "@aws-sdk/client-kms": "^3.362.0", "tslib": "^2.2.0" } @@ -636,13 +666,26 @@ "@aws-crypto/material-management": "file:../material-management", "asn1.js": "^5.3.0", "bn.js": "^5.1.1", - "tslib": "^2.2.0" + "tslib": "^2.2.0", + "uuid": "^10.0.0" } }, "modules/serialize/node_modules/bn.js": { "version": "5.2.1", "license": "MIT" }, + "modules/serialize/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "modules/web-crypto-backend": { "name": "@aws-crypto/web-crypto-backend", "version": "4.0.1", @@ -689,6 +732,10 @@ "node": ">=6.0.0" } }, + "node_modules/@aws-crypto/branch-keystore-node": { + "resolved": "modules/branch-keystore-node", + "link": true + }, "node_modules/@aws-crypto/cache-material": { "resolved": "modules/cache-material", "link": true @@ -749,6 +796,10 @@ "resolved": "modules/integration-vectors", "link": true }, + "node_modules/@aws-crypto/kdf-ctr-mode-node": { + "resolved": "modules/kdf-ctr-mode-node", + "link": true + }, "node_modules/@aws-crypto/kms-keyring": { "resolved": "modules/kms-keyring", "link": true @@ -937,6 +988,530 @@ "resolved": "modules/web-crypto-backend", "link": true }, + "node_modules/@aws-sdk/client-dynamodb": { + "version": "3.712.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.712.0.tgz", + "integrity": "sha512-BCIKfjkItIM8eP6/QOP+DD89xYLw0jTTgErSMq6tmSGf4PKtVk3VV4GyKqEm9vKBzbz0/7068YADKALd5Uv4nA==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.712.0", + "@aws-sdk/client-sts": "3.712.0", + "@aws-sdk/core": "3.709.0", + "@aws-sdk/credential-provider-node": "3.712.0", + "@aws-sdk/middleware-endpoint-discovery": "3.709.0", + "@aws-sdk/middleware-host-header": "3.709.0", + "@aws-sdk/middleware-logger": "3.709.0", + "@aws-sdk/middleware-recursion-detection": "3.709.0", + "@aws-sdk/middleware-user-agent": "3.709.0", + "@aws-sdk/region-config-resolver": "3.709.0", + "@aws-sdk/types": "3.709.0", + "@aws-sdk/util-endpoints": "3.709.0", + "@aws-sdk/util-user-agent-browser": "3.709.0", + "@aws-sdk/util-user-agent-node": "3.712.0", + "@smithy/config-resolver": "^3.0.13", + "@smithy/core": "^2.5.5", + "@smithy/fetch-http-handler": "^4.1.2", + "@smithy/hash-node": "^3.0.11", + "@smithy/invalid-dependency": "^3.0.11", + "@smithy/middleware-content-length": "^3.0.13", + "@smithy/middleware-endpoint": "^3.2.5", + "@smithy/middleware-retry": "^3.0.30", + "@smithy/middleware-serde": "^3.0.11", + "@smithy/middleware-stack": "^3.0.11", + "@smithy/node-config-provider": "^3.1.12", + "@smithy/node-http-handler": "^3.3.2", + "@smithy/protocol-http": "^4.1.8", + "@smithy/smithy-client": "^3.5.0", + "@smithy/types": "^3.7.2", + "@smithy/url-parser": "^3.0.11", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.30", + "@smithy/util-defaults-mode-node": "^3.0.30", + "@smithy/util-endpoints": "^2.1.7", + "@smithy/util-middleware": "^3.0.11", + "@smithy/util-retry": "^3.0.11", + "@smithy/util-utf8": "^3.0.0", + "@smithy/util-waiter": "^3.2.0", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/client-sso": { + "version": "3.712.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.712.0.tgz", + "integrity": "sha512-tBo/eW3YpZ9f3Q1qA7aA8uliNFJJX0OP7R2IUJ8t6rqVTk15wWCEPNmXzUZKgruDnKUfCaF4+r9q/Yy4fBc9PA==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.709.0", + "@aws-sdk/middleware-host-header": "3.709.0", + "@aws-sdk/middleware-logger": "3.709.0", + "@aws-sdk/middleware-recursion-detection": "3.709.0", + "@aws-sdk/middleware-user-agent": "3.709.0", + "@aws-sdk/region-config-resolver": "3.709.0", + "@aws-sdk/types": "3.709.0", + "@aws-sdk/util-endpoints": "3.709.0", + "@aws-sdk/util-user-agent-browser": "3.709.0", + "@aws-sdk/util-user-agent-node": "3.712.0", + "@smithy/config-resolver": "^3.0.13", + "@smithy/core": "^2.5.5", + "@smithy/fetch-http-handler": "^4.1.2", + "@smithy/hash-node": "^3.0.11", + "@smithy/invalid-dependency": "^3.0.11", + "@smithy/middleware-content-length": "^3.0.13", + "@smithy/middleware-endpoint": "^3.2.5", + "@smithy/middleware-retry": "^3.0.30", + "@smithy/middleware-serde": "^3.0.11", + "@smithy/middleware-stack": "^3.0.11", + "@smithy/node-config-provider": "^3.1.12", + "@smithy/node-http-handler": "^3.3.2", + "@smithy/protocol-http": "^4.1.8", + "@smithy/smithy-client": "^3.5.0", + "@smithy/types": "^3.7.2", + "@smithy/url-parser": "^3.0.11", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.30", + "@smithy/util-defaults-mode-node": "^3.0.30", + "@smithy/util-endpoints": "^2.1.7", + "@smithy/util-middleware": "^3.0.11", + "@smithy/util-retry": "^3.0.11", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.712.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.712.0.tgz", + "integrity": "sha512-xNFrG9syrG6pxUP7Ld/nu3afQ9+rbJM9qrE+wDNz4VnNZ3vLiJty4fH85zBFhOQ5OF2DIJTWsFzXGi2FYjsCMA==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.709.0", + "@aws-sdk/credential-provider-node": "3.712.0", + "@aws-sdk/middleware-host-header": "3.709.0", + "@aws-sdk/middleware-logger": "3.709.0", + "@aws-sdk/middleware-recursion-detection": "3.709.0", + "@aws-sdk/middleware-user-agent": "3.709.0", + "@aws-sdk/region-config-resolver": "3.709.0", + "@aws-sdk/types": "3.709.0", + "@aws-sdk/util-endpoints": "3.709.0", + "@aws-sdk/util-user-agent-browser": "3.709.0", + "@aws-sdk/util-user-agent-node": "3.712.0", + "@smithy/config-resolver": "^3.0.13", + "@smithy/core": "^2.5.5", + "@smithy/fetch-http-handler": "^4.1.2", + "@smithy/hash-node": "^3.0.11", + "@smithy/invalid-dependency": "^3.0.11", + "@smithy/middleware-content-length": "^3.0.13", + "@smithy/middleware-endpoint": "^3.2.5", + "@smithy/middleware-retry": "^3.0.30", + "@smithy/middleware-serde": "^3.0.11", + "@smithy/middleware-stack": "^3.0.11", + "@smithy/node-config-provider": "^3.1.12", + "@smithy/node-http-handler": "^3.3.2", + "@smithy/protocol-http": "^4.1.8", + "@smithy/smithy-client": "^3.5.0", + "@smithy/types": "^3.7.2", + "@smithy/url-parser": "^3.0.11", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.30", + "@smithy/util-defaults-mode-node": "^3.0.30", + "@smithy/util-endpoints": "^2.1.7", + "@smithy/util-middleware": "^3.0.11", + "@smithy/util-retry": "^3.0.11", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.712.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/client-sts": { + "version": "3.712.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.712.0.tgz", + "integrity": "sha512-gIO6BD+hkEe3GKQhbiFP0zcNQv0EkP1Cl9SOstxS+X9CeudEgVX/xEPUjyoFVkfkntPBJ1g0I1u5xOzzRExl4g==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.712.0", + "@aws-sdk/core": "3.709.0", + "@aws-sdk/credential-provider-node": "3.712.0", + "@aws-sdk/middleware-host-header": "3.709.0", + "@aws-sdk/middleware-logger": "3.709.0", + "@aws-sdk/middleware-recursion-detection": "3.709.0", + "@aws-sdk/middleware-user-agent": "3.709.0", + "@aws-sdk/region-config-resolver": "3.709.0", + "@aws-sdk/types": "3.709.0", + "@aws-sdk/util-endpoints": "3.709.0", + "@aws-sdk/util-user-agent-browser": "3.709.0", + "@aws-sdk/util-user-agent-node": "3.712.0", + "@smithy/config-resolver": "^3.0.13", + "@smithy/core": "^2.5.5", + "@smithy/fetch-http-handler": "^4.1.2", + "@smithy/hash-node": "^3.0.11", + "@smithy/invalid-dependency": "^3.0.11", + "@smithy/middleware-content-length": "^3.0.13", + "@smithy/middleware-endpoint": "^3.2.5", + "@smithy/middleware-retry": "^3.0.30", + "@smithy/middleware-serde": "^3.0.11", + "@smithy/middleware-stack": "^3.0.11", + "@smithy/node-config-provider": "^3.1.12", + "@smithy/node-http-handler": "^3.3.2", + "@smithy/protocol-http": "^4.1.8", + "@smithy/smithy-client": "^3.5.0", + "@smithy/types": "^3.7.2", + "@smithy/url-parser": "^3.0.11", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.30", + "@smithy/util-defaults-mode-node": "^3.0.30", + "@smithy/util-endpoints": "^2.1.7", + "@smithy/util-middleware": "^3.0.11", + "@smithy/util-retry": "^3.0.11", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/core": { + "version": "3.709.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.709.0.tgz", + "integrity": "sha512-7kuSpzdOTAE026j85wq/fN9UDZ70n0OHw81vFqMWwlEFtm5IQ/MRCLKcC4HkXxTdfy1PqFlmoXxWqeBa15tujw==", + "dependencies": { + "@aws-sdk/types": "3.709.0", + "@smithy/core": "^2.5.5", + "@smithy/node-config-provider": "^3.1.12", + "@smithy/property-provider": "^3.1.11", + "@smithy/protocol-http": "^4.1.8", + "@smithy/signature-v4": "^4.2.4", + "@smithy/smithy-client": "^3.5.0", + "@smithy/types": "^3.7.2", + "@smithy/util-middleware": "^3.0.11", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.709.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.709.0.tgz", + "integrity": "sha512-ZMAp9LSikvHDFVa84dKpQmow6wsg956Um20cKuioPpX2GGreJFur7oduD+tRJT6FtIOHn+64YH+0MwiXLhsaIQ==", + "dependencies": { + "@aws-sdk/core": "3.709.0", + "@aws-sdk/types": "3.709.0", + "@smithy/property-provider": "^3.1.11", + "@smithy/types": "^3.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.709.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.709.0.tgz", + "integrity": "sha512-lIS7XLwCOyJnLD70f+VIRr8DNV1HPQe9oN6aguYrhoczqz7vDiVZLe3lh714cJqq9rdxzFypK5DqKHmcscMEPQ==", + "dependencies": { + "@aws-sdk/core": "3.709.0", + "@aws-sdk/types": "3.709.0", + "@smithy/fetch-http-handler": "^4.1.2", + "@smithy/node-http-handler": "^3.3.2", + "@smithy/property-provider": "^3.1.11", + "@smithy/protocol-http": "^4.1.8", + "@smithy/smithy-client": "^3.5.0", + "@smithy/types": "^3.7.2", + "@smithy/util-stream": "^3.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.712.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.712.0.tgz", + "integrity": "sha512-sTsdQ/Fm/suqMdpjhMuss/5uKL18vcuWnNTQVrG9iGNRqZLbq65MXquwbUpgzfoUmIcH+4CrY6H2ebpTIECIag==", + "dependencies": { + "@aws-sdk/core": "3.709.0", + "@aws-sdk/credential-provider-env": "3.709.0", + "@aws-sdk/credential-provider-http": "3.709.0", + "@aws-sdk/credential-provider-process": "3.709.0", + "@aws-sdk/credential-provider-sso": "3.712.0", + "@aws-sdk/credential-provider-web-identity": "3.709.0", + "@aws-sdk/types": "3.709.0", + "@smithy/credential-provider-imds": "^3.2.8", + "@smithy/property-provider": "^3.1.11", + "@smithy/shared-ini-file-loader": "^3.1.12", + "@smithy/types": "^3.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.712.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.712.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.712.0.tgz", + "integrity": "sha512-gXrHymW3rMRYORkPVQwL8Gi5Lu92F16SoZR543x03qCi7rm00oL9tRD85ACxkhprS1Wh8lUIUMNoeiwnYWTNuQ==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.709.0", + "@aws-sdk/credential-provider-http": "3.709.0", + "@aws-sdk/credential-provider-ini": "3.712.0", + "@aws-sdk/credential-provider-process": "3.709.0", + "@aws-sdk/credential-provider-sso": "3.712.0", + "@aws-sdk/credential-provider-web-identity": "3.709.0", + "@aws-sdk/types": "3.709.0", + "@smithy/credential-provider-imds": "^3.2.8", + "@smithy/property-provider": "^3.1.11", + "@smithy/shared-ini-file-loader": "^3.1.12", + "@smithy/types": "^3.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.709.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.709.0.tgz", + "integrity": "sha512-IAC+jPlGQII6jhIylHOwh3RgSobqlgL59nw2qYTURr8hMCI0Z1p5y2ee646HTVt4WeCYyzUAXfxr6YI/Vitv+Q==", + "dependencies": { + "@aws-sdk/core": "3.709.0", + "@aws-sdk/types": "3.709.0", + "@smithy/property-provider": "^3.1.11", + "@smithy/shared-ini-file-loader": "^3.1.12", + "@smithy/types": "^3.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.712.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.712.0.tgz", + "integrity": "sha512-8lCMxY7Lb9VK9qdlNXRJXE3W1UDVURnJZ3a4XWYNY6yr1TfQaN40mMyXX1oNlXXJtMV0szRvjM8dZj37E/ESAw==", + "dependencies": { + "@aws-sdk/client-sso": "3.712.0", + "@aws-sdk/core": "3.709.0", + "@aws-sdk/token-providers": "3.709.0", + "@aws-sdk/types": "3.709.0", + "@smithy/property-provider": "^3.1.11", + "@smithy/shared-ini-file-loader": "^3.1.12", + "@smithy/types": "^3.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.709.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.709.0.tgz", + "integrity": "sha512-2lbDfE0IQ6gma/7BB2JpkjW5G0wGe4AS0x80oybYAYYviJmUtIR3Cn2pXun6bnAWElt4wYKl4su7oC36rs5rNA==", + "dependencies": { + "@aws-sdk/core": "3.709.0", + "@aws-sdk/types": "3.709.0", + "@smithy/property-provider": "^3.1.11", + "@smithy/types": "^3.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.709.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.709.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.709.0.tgz", + "integrity": "sha512-8gQYCYAaIw4lOCd5WYdf15Y/61MgRsAnrb2eiTl+icMlUOOzl8aOl5iDwm/Idp0oHZTflwxM4XSvGXO83PRWcw==", + "dependencies": { + "@aws-sdk/types": "3.709.0", + "@smithy/protocol-http": "^4.1.8", + "@smithy/types": "^3.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/middleware-logger": { + "version": "3.709.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.709.0.tgz", + "integrity": "sha512-jDoGSccXv9zebnpUoisjWd5u5ZPIalrmm6TjvPzZ8UqzQt3Beiz0tnQwmxQD6KRc7ADweWP5Ntiqzbw9xpVajg==", + "dependencies": { + "@aws-sdk/types": "3.709.0", + "@smithy/types": "^3.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.709.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.709.0.tgz", + "integrity": "sha512-PObL/wLr4lkfbQ0yXUWaoCWu/jcwfwZzCjsUiXW/H6hW9b/00enZxmx7OhtJYaR6xmh/Lcx5wbhIoDCbzdv0tw==", + "dependencies": { + "@aws-sdk/types": "3.709.0", + "@smithy/protocol-http": "^4.1.8", + "@smithy/types": "^3.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.709.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.709.0.tgz", + "integrity": "sha512-ooc9ZJvgkjPhi9q05XwSfNTXkEBEIfL4hleo5rQBKwHG3aTHvwOM7LLzhdX56QZVa6sorPBp6fwULuRDSqiQHw==", + "dependencies": { + "@aws-sdk/core": "3.709.0", + "@aws-sdk/types": "3.709.0", + "@aws-sdk/util-endpoints": "3.709.0", + "@smithy/core": "^2.5.5", + "@smithy/protocol-http": "^4.1.8", + "@smithy/types": "^3.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.709.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.709.0.tgz", + "integrity": "sha512-/NoCAMEVKAg3kBKOrNtgOfL+ECt6nrl+L7q2SyYmrcY4tVCmwuECVqewQaHc03fTnJijfKLccw0Fj+6wOCnB6w==", + "dependencies": { + "@aws-sdk/types": "3.709.0", + "@smithy/node-config-provider": "^3.1.12", + "@smithy/types": "^3.7.2", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.11", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/token-providers": { + "version": "3.709.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.709.0.tgz", + "integrity": "sha512-q5Ar6k71nci43IbULFgC8a89d/3EHpmd7HvBzqVGRcHnoPwh8eZDBfbBXKH83NGwcS1qPSRYiDbVfeWPm4/1jA==", + "dependencies": { + "@aws-sdk/types": "3.709.0", + "@smithy/property-provider": "^3.1.11", + "@smithy/shared-ini-file-loader": "^3.1.12", + "@smithy/types": "^3.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sso-oidc": "^3.709.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/types": { + "version": "3.709.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.709.0.tgz", + "integrity": "sha512-ArtLTMxgjf13Kfu3gWH3Ez9Q5TkDdcRZUofpKH3pMGB/C6KAbeSCtIIDKfoRTUABzyGlPyCrZdnFjKyH+ypIpg==", + "dependencies": { + "@smithy/types": "^3.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/util-endpoints": { + "version": "3.709.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.709.0.tgz", + "integrity": "sha512-Mbc7AtL5WGCTKC16IGeUTz+sjpC3ptBda2t0CcK0kMVw3THDdcSq6ZlNKO747cNqdbwUvW34oHteUiHv4/z88Q==", + "dependencies": { + "@aws-sdk/types": "3.709.0", + "@smithy/types": "^3.7.2", + "@smithy/util-endpoints": "^2.1.7", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.709.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.709.0.tgz", + "integrity": "sha512-/rL2GasJzdTWUURCQKFldw2wqBtY4k4kCiA2tVZSKg3y4Ey7zO34SW8ebaeCE2/xoWOyLR2/etdKyphoo4Zrtg==", + "dependencies": { + "@aws-sdk/types": "3.709.0", + "@smithy/types": "^3.7.2", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.712.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.712.0.tgz", + "integrity": "sha512-26X21bZ4FWsVpqs33uOXiB60TOWQdVlr7T7XONDFL/XN7GEpUJkWuuIB4PTok6VOmh1viYcdxZQqekXPuzXexQ==", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.709.0", + "@aws-sdk/types": "3.709.0", + "@smithy/node-config-provider": "^3.1.12", + "@smithy/types": "^3.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@smithy/fetch-http-handler": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-4.1.2.tgz", + "integrity": "sha512-R7rU7Ae3ItU4rC0c5mB2sP5mJNbCfoDc8I5XlYjIZnquyUwec7fEo78F6DA3SmgJgkU1qTMcZJuGblxZsl10ZA==", + "dependencies": { + "@smithy/protocol-http": "^4.1.8", + "@smithy/querystring-builder": "^3.0.11", + "@smithy/types": "^3.7.2", + "@smithy/util-base64": "^3.0.0", + "tslib": "^2.6.2" + } + }, "node_modules/@aws-sdk/client-kms": { "version": "3.637.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-kms/-/client-kms-3.637.0.tgz", @@ -1286,6 +1861,46 @@ "node": ">=16.0.0" } }, + "node_modules/@aws-sdk/endpoint-cache": { + "version": "3.693.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/endpoint-cache/-/endpoint-cache-3.693.0.tgz", + "integrity": "sha512-/zK0ZZncBf5FbTfo8rJMcQIXXk4Ibhe5zEMiwFNivVPR2uNC0+oqfwXz7vjxwY0t6BPE3Bs4h9uFEz4xuGCY6w==", + "dependencies": { + "mnemonist": "0.38.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-endpoint-discovery": { + "version": "3.709.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-endpoint-discovery/-/middleware-endpoint-discovery-3.709.0.tgz", + "integrity": "sha512-6CSHoAy3sVBJdeGiBpoRqVHpqLPqv5QuDxKsEMHoGdbGATmffyn2whTFfo5hfRYsN9WPz/XxUX2iynqQCnlrzw==", + "dependencies": { + "@aws-sdk/endpoint-cache": "3.693.0", + "@aws-sdk/types": "3.709.0", + "@smithy/node-config-provider": "^3.1.12", + "@smithy/protocol-http": "^4.1.8", + "@smithy/types": "^3.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-endpoint-discovery/node_modules/@aws-sdk/types": { + "version": "3.709.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.709.0.tgz", + "integrity": "sha512-ArtLTMxgjf13Kfu3gWH3Ez9Q5TkDdcRZUofpKH3pMGB/C6KAbeSCtIIDKfoRTUABzyGlPyCrZdnFjKyH+ypIpg==", + "dependencies": { + "@smithy/types": "^3.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/@aws-sdk/middleware-host-header": { "version": "3.620.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.620.0.tgz", @@ -1453,6 +2068,20 @@ "node": ">=14.0.0" } }, + "node_modules/@aws-sdk/util-dynamodb": { + "version": "3.712.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-dynamodb/-/util-dynamodb-3.712.0.tgz", + "integrity": "sha512-YYy2+1Cey3SrdM6DWZtnkikxdu4wpHhUXXxN7P1WQV/ZURw7AeavowfW3BPS1hkmM/nVNU+ahx2hBOEMxxU1MA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-dynamodb": "^3.712.0" + } + }, "node_modules/@aws-sdk/util-endpoints": { "version": "3.637.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.637.0.tgz", @@ -3900,12 +4529,11 @@ } }, "node_modules/@smithy/abort-controller": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-3.1.1.tgz", - "integrity": "sha512-MBJBiidoe+0cTFhyxT8g+9g7CeVccLM0IOKKUMCNQ1CNMJ/eIfoo0RTfVrXOONEI1UCN1W+zkiHSbzUNE9dZtQ==", - "license": "Apache-2.0", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-3.1.9.tgz", + "integrity": "sha512-yiW0WI30zj8ZKoSYNx90no7ugVn3khlyH/z5W8qtKBtVE6awRALbhSG+2SAHA1r6bO/6M9utxYKVZ3PCJ1rWxw==", "dependencies": { - "@smithy/types": "^3.3.0", + "@smithy/types": "^3.7.2", "tslib": "^2.6.2" }, "engines": { @@ -3913,15 +4541,14 @@ } }, "node_modules/@smithy/config-resolver": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-3.0.5.tgz", - "integrity": "sha512-SkW5LxfkSI1bUC74OtfBbdz+grQXYiPYolyu8VfpLIjEoN/sHVBlLeGXMQ1vX4ejkgfv6sxVbQJ32yF2cl1veA==", - "license": "Apache-2.0", + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-3.0.13.tgz", + "integrity": "sha512-Gr/qwzyPaTL1tZcq8WQyHhTZREER5R1Wytmz4WnVGL4onA3dNk6Btll55c8Vr58pLdvWZmtG8oZxJTw3t3q7Jg==", "dependencies": { - "@smithy/node-config-provider": "^3.1.4", - "@smithy/types": "^3.3.0", + "@smithy/node-config-provider": "^3.1.12", + "@smithy/types": "^3.7.2", "@smithy/util-config-provider": "^3.0.0", - "@smithy/util-middleware": "^3.0.3", + "@smithy/util-middleware": "^3.0.11", "tslib": "^2.6.2" }, "engines": { @@ -3929,18 +4556,16 @@ } }, "node_modules/@smithy/core": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-2.4.0.tgz", - "integrity": "sha512-cHXq+FneIF/KJbt4q4pjN186+Jf4ZB0ZOqEaZMBhT79srEyGDDBV31NqBRBjazz8ppQ1bJbDJMY9ba5wKFV36w==", + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-2.5.5.tgz", + "integrity": "sha512-G8G/sDDhXA7o0bOvkc7bgai6POuSld/+XhNnWAbpQTpLv2OZPvyqQ58tLPPlz0bSNsXktldDDREIv1LczFeNEw==", "dependencies": { - "@smithy/middleware-endpoint": "^3.1.0", - "@smithy/middleware-retry": "^3.0.15", - "@smithy/middleware-serde": "^3.0.3", - "@smithy/protocol-http": "^4.1.0", - "@smithy/smithy-client": "^3.2.0", - "@smithy/types": "^3.3.0", + "@smithy/middleware-serde": "^3.0.11", + "@smithy/protocol-http": "^4.1.8", + "@smithy/types": "^3.7.2", "@smithy/util-body-length-browser": "^3.0.0", - "@smithy/util-middleware": "^3.0.3", + "@smithy/util-middleware": "^3.0.11", + "@smithy/util-stream": "^3.3.2", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" }, @@ -3949,14 +4574,14 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-3.2.0.tgz", - "integrity": "sha512-0SCIzgd8LYZ9EJxUjLXBmEKSZR/P/w6l7Rz/pab9culE/RWuqelAKGJvn5qUOl8BgX8Yj5HWM50A5hiB/RzsgA==", - "dependencies": { - "@smithy/node-config-provider": "^3.1.4", - "@smithy/property-provider": "^3.1.3", - "@smithy/types": "^3.3.0", - "@smithy/url-parser": "^3.0.3", + "version": "3.2.8", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-3.2.8.tgz", + "integrity": "sha512-ZCY2yD0BY+K9iMXkkbnjo+08T2h8/34oHd0Jmh6BZUSZwaaGlGCyBT/3wnS7u7Xl33/EEfN4B6nQr3Gx5bYxgw==", + "dependencies": { + "@smithy/node-config-provider": "^3.1.12", + "@smithy/property-provider": "^3.1.11", + "@smithy/types": "^3.7.2", + "@smithy/url-parser": "^3.0.11", "tslib": "^2.6.2" }, "engines": { @@ -3976,12 +4601,11 @@ } }, "node_modules/@smithy/hash-node": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-3.0.3.tgz", - "integrity": "sha512-2ctBXpPMG+B3BtWSGNnKELJ7SH9e4TNefJS0cd2eSkOOROeBnnVBnAy9LtJ8tY4vUEoe55N4CNPxzbWvR39iBw==", - "license": "Apache-2.0", + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-3.0.11.tgz", + "integrity": "sha512-emP23rwYyZhQBvklqTtwetkQlqbNYirDiEEwXl2v0GYWMnCzxst7ZaRAnWuy28njp5kAH54lvkdG37MblZzaHA==", "dependencies": { - "@smithy/types": "^3.3.0", + "@smithy/types": "^3.7.2", "@smithy/util-buffer-from": "^3.0.0", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" @@ -3991,12 +4615,11 @@ } }, "node_modules/@smithy/invalid-dependency": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-3.0.3.tgz", - "integrity": "sha512-ID1eL/zpDULmHJbflb864k72/SNOZCADRc9i7Exq3RUNJw6raWUSlFEQ+3PX3EYs++bTxZB2dE9mEHTQLv61tw==", - "license": "Apache-2.0", + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-3.0.11.tgz", + "integrity": "sha512-NuQmVPEJjUX6c+UELyVz8kUx8Q539EDeNwbRyu4IIF8MeV7hUtq1FB3SHVyki2u++5XLMFqngeMKk7ccspnNyQ==", "dependencies": { - "@smithy/types": "^3.3.0", + "@smithy/types": "^3.7.2", "tslib": "^2.6.2" } }, @@ -4013,12 +4636,12 @@ } }, "node_modules/@smithy/middleware-content-length": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-3.0.5.tgz", - "integrity": "sha512-ILEzC2eyxx6ncej3zZSwMpB5RJ0zuqH7eMptxC4KN3f+v9bqT8ohssKbhNR78k/2tWW+KS5Spw+tbPF4Ejyqvw==", + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-3.0.13.tgz", + "integrity": "sha512-zfMhzojhFpIX3P5ug7jxTjfUcIPcGjcQYzB9t+rv0g1TX7B0QdwONW+ATouaLoD7h7LOw/ZlXfkq4xJ/g2TrIw==", "dependencies": { - "@smithy/protocol-http": "^4.1.0", - "@smithy/types": "^3.3.0", + "@smithy/protocol-http": "^4.1.8", + "@smithy/types": "^3.7.2", "tslib": "^2.6.2" }, "engines": { @@ -4026,16 +4649,17 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-3.1.0.tgz", - "integrity": "sha512-5y5aiKCEwg9TDPB4yFE7H6tYvGFf1OJHNczeY10/EFF8Ir8jZbNntQJxMWNfeQjC1mxPsaQ6mR9cvQbf+0YeMw==", - "dependencies": { - "@smithy/middleware-serde": "^3.0.3", - "@smithy/node-config-provider": "^3.1.4", - "@smithy/shared-ini-file-loader": "^3.1.4", - "@smithy/types": "^3.3.0", - "@smithy/url-parser": "^3.0.3", - "@smithy/util-middleware": "^3.0.3", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-3.2.5.tgz", + "integrity": "sha512-VhJNs/s/lyx4weiZdXSloBgoLoS8osV0dKIain8nGmx7of3QFKu5BSdEuk1z/U8x9iwes1i+XCiNusEvuK1ijg==", + "dependencies": { + "@smithy/core": "^2.5.5", + "@smithy/middleware-serde": "^3.0.11", + "@smithy/node-config-provider": "^3.1.12", + "@smithy/shared-ini-file-loader": "^3.1.12", + "@smithy/types": "^3.7.2", + "@smithy/url-parser": "^3.0.11", + "@smithy/util-middleware": "^3.0.11", "tslib": "^2.6.2" }, "engines": { @@ -4043,17 +4667,17 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-3.0.15.tgz", - "integrity": "sha512-iTMedvNt1ApdvkaoE8aSDuwaoc+BhvHqttbA/FO4Ty+y/S5hW6Ci/CTScG7vam4RYJWZxdTElc3MEfHRVH6cgQ==", - "dependencies": { - "@smithy/node-config-provider": "^3.1.4", - "@smithy/protocol-http": "^4.1.0", - "@smithy/service-error-classification": "^3.0.3", - "@smithy/smithy-client": "^3.2.0", - "@smithy/types": "^3.3.0", - "@smithy/util-middleware": "^3.0.3", - "@smithy/util-retry": "^3.0.3", + "version": "3.0.30", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-3.0.30.tgz", + "integrity": "sha512-6323RL2BvAR3VQpTjHpa52kH/iSHyxd/G9ohb2MkBk2Ucu+oMtRXT8yi7KTSIS9nb58aupG6nO0OlXnQOAcvmQ==", + "dependencies": { + "@smithy/node-config-provider": "^3.1.12", + "@smithy/protocol-http": "^4.1.8", + "@smithy/service-error-classification": "^3.0.11", + "@smithy/smithy-client": "^3.5.0", + "@smithy/types": "^3.7.2", + "@smithy/util-middleware": "^3.0.11", + "@smithy/util-retry": "^3.0.11", "tslib": "^2.6.2", "uuid": "^9.0.1" }, @@ -4062,12 +4686,11 @@ } }, "node_modules/@smithy/middleware-serde": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-3.0.3.tgz", - "integrity": "sha512-puUbyJQBcg9eSErFXjKNiGILJGtiqmuuNKEYNYfUD57fUl4i9+mfmThtQhvFXU0hCVG0iEJhvQUipUf+/SsFdA==", - "license": "Apache-2.0", + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-3.0.11.tgz", + "integrity": "sha512-KzPAeySp/fOoQA82TpnwItvX8BBURecpx6ZMu75EZDkAcnPtO6vf7q4aH5QHs/F1s3/snQaSFbbUMcFFZ086Mw==", "dependencies": { - "@smithy/types": "^3.3.0", + "@smithy/types": "^3.7.2", "tslib": "^2.6.2" }, "engines": { @@ -4075,12 +4698,11 @@ } }, "node_modules/@smithy/middleware-stack": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-3.0.3.tgz", - "integrity": "sha512-r4klY9nFudB0r9UdSMaGSyjyQK5adUyPnQN/ZM6M75phTxOdnc/AhpvGD1fQUvgmqjQEBGCwpnPbDm8pH5PapA==", - "license": "Apache-2.0", + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-3.0.11.tgz", + "integrity": "sha512-1HGo9a6/ikgOMrTrWL/WiN9N8GSVYpuRQO5kjstAq4CvV59bjqnh7TbdXGQ4vxLD3xlSjfBjq5t1SOELePsLnA==", "dependencies": { - "@smithy/types": "^3.3.0", + "@smithy/types": "^3.7.2", "tslib": "^2.6.2" }, "engines": { @@ -4088,14 +4710,13 @@ } }, "node_modules/@smithy/node-config-provider": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-3.1.4.tgz", - "integrity": "sha512-YvnElQy8HR4vDcAjoy7Xkx9YT8xZP4cBXcbJSgm/kxmiQu08DwUwj8rkGnyoJTpfl/3xYHH+d8zE+eHqoDCSdQ==", - "license": "Apache-2.0", + "version": "3.1.12", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-3.1.12.tgz", + "integrity": "sha512-O9LVEu5J/u/FuNlZs+L7Ikn3lz7VB9hb0GtPT9MQeiBmtK8RSY3ULmsZgXhe6VAlgTw0YO+paQx4p8xdbs43vQ==", "dependencies": { - "@smithy/property-provider": "^3.1.3", - "@smithy/shared-ini-file-loader": "^3.1.4", - "@smithy/types": "^3.3.0", + "@smithy/property-provider": "^3.1.11", + "@smithy/shared-ini-file-loader": "^3.1.12", + "@smithy/types": "^3.7.2", "tslib": "^2.6.2" }, "engines": { @@ -4103,14 +4724,14 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-3.1.4.tgz", - "integrity": "sha512-+UmxgixgOr/yLsUxcEKGH0fMNVteJFGkmRltYFHnBMlogyFdpzn2CwqWmxOrfJELhV34v0WSlaqG1UtE1uXlJg==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-3.3.2.tgz", + "integrity": "sha512-t4ng1DAd527vlxvOfKFYEe6/QFBcsj7WpNlWTyjorwXXcKw3XlltBGbyHfSJ24QT84nF+agDha9tNYpzmSRZPA==", "dependencies": { - "@smithy/abort-controller": "^3.1.1", - "@smithy/protocol-http": "^4.1.0", - "@smithy/querystring-builder": "^3.0.3", - "@smithy/types": "^3.3.0", + "@smithy/abort-controller": "^3.1.9", + "@smithy/protocol-http": "^4.1.8", + "@smithy/querystring-builder": "^3.0.11", + "@smithy/types": "^3.7.2", "tslib": "^2.6.2" }, "engines": { @@ -4118,12 +4739,11 @@ } }, "node_modules/@smithy/property-provider": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-3.1.3.tgz", - "integrity": "sha512-zahyOVR9Q4PEoguJ/NrFP4O7SMAfYO1HLhB18M+q+Z4KFd4V2obiMnlVoUFzFLSPeVt1POyNWneHHrZaTMoc/g==", - "license": "Apache-2.0", + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-3.1.11.tgz", + "integrity": "sha512-I/+TMc4XTQ3QAjXfOcUWbSS073oOEAxgx4aZy8jHaf8JQnRkq2SZWw8+PfDtBvLUjcGMdxl+YwtzWe6i5uhL/A==", "dependencies": { - "@smithy/types": "^3.3.0", + "@smithy/types": "^3.7.2", "tslib": "^2.6.2" }, "engines": { @@ -4131,11 +4751,11 @@ } }, "node_modules/@smithy/protocol-http": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-4.1.0.tgz", - "integrity": "sha512-dPVoHYQ2wcHooGXg3LQisa1hH0e4y0pAddPMeeUPipI1tEOqL6A4N0/G7abeq+K8wrwSgjk4C0wnD1XZpJm5aA==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-4.1.8.tgz", + "integrity": "sha512-hmgIAVyxw1LySOwkgMIUN0kjN8TG9Nc85LJeEmEE/cNEe2rkHDUWhnJf2gxcSRFLWsyqWsrZGw40ROjUogg+Iw==", "dependencies": { - "@smithy/types": "^3.3.0", + "@smithy/types": "^3.7.2", "tslib": "^2.6.2" }, "engines": { @@ -4143,12 +4763,11 @@ } }, "node_modules/@smithy/querystring-builder": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-3.0.3.tgz", - "integrity": "sha512-vyWckeUeesFKzCDaRwWLUA1Xym9McaA6XpFfAK5qI9DKJ4M33ooQGqvM4J+LalH4u/Dq9nFiC8U6Qn1qi0+9zw==", - "license": "Apache-2.0", + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-3.0.11.tgz", + "integrity": "sha512-u+5HV/9uJaeLj5XTb6+IEF/dokWWkEqJ0XiaRRogyREmKGUgZnNecLucADLdauWFKUNbQfulHFEZEdjwEBjXRg==", "dependencies": { - "@smithy/types": "^3.3.0", + "@smithy/types": "^3.7.2", "@smithy/util-uri-escape": "^3.0.0", "tslib": "^2.6.2" }, @@ -4157,12 +4776,11 @@ } }, "node_modules/@smithy/querystring-parser": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-3.0.3.tgz", - "integrity": "sha512-zahM1lQv2YjmznnfQsWbYojFe55l0SLG/988brlLv1i8z3dubloLF+75ATRsqPBboUXsW6I9CPGE5rQgLfY0vQ==", - "license": "Apache-2.0", + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-3.0.11.tgz", + "integrity": "sha512-Je3kFvCsFMnso1ilPwA7GtlbPaTixa3WwC+K21kmMZHsBEOZYQaqxcMqeFFoU7/slFjKDIpiiPydvdJm8Q/MCw==", "dependencies": { - "@smithy/types": "^3.3.0", + "@smithy/types": "^3.7.2", "tslib": "^2.6.2" }, "engines": { @@ -4170,24 +4788,22 @@ } }, "node_modules/@smithy/service-error-classification": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-3.0.3.tgz", - "integrity": "sha512-Jn39sSl8cim/VlkLsUhRFq/dKDnRUFlfRkvhOJaUbLBXUsLRLNf9WaxDv/z9BjuQ3A6k/qE8af1lsqcwm7+DaQ==", - "license": "Apache-2.0", + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-3.0.11.tgz", + "integrity": "sha512-QnYDPkyewrJzCyaeI2Rmp7pDwbUETe+hU8ADkXmgNusO1bgHBH7ovXJiYmba8t0fNfJx75fE8dlM6SEmZxheog==", "dependencies": { - "@smithy/types": "^3.3.0" + "@smithy/types": "^3.7.2" }, "engines": { "node": ">=16.0.0" } }, "node_modules/@smithy/shared-ini-file-loader": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-3.1.4.tgz", - "integrity": "sha512-qMxS4hBGB8FY2GQqshcRUy1K6k8aBWP5vwm8qKkCT3A9K2dawUwOIJfqh9Yste/Bl0J2lzosVyrXDj68kLcHXQ==", - "license": "Apache-2.0", + "version": "3.1.12", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-3.1.12.tgz", + "integrity": "sha512-1xKSGI+U9KKdbG2qDvIR9dGrw3CNx+baqJfyr0igKEpjbHL5stsqAesYBzHChYHlelWtb87VnLWlhvfCz13H8Q==", "dependencies": { - "@smithy/types": "^3.3.0", + "@smithy/types": "^3.7.2", "tslib": "^2.6.2" }, "engines": { @@ -4195,15 +4811,15 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-4.1.0.tgz", - "integrity": "sha512-aRryp2XNZeRcOtuJoxjydO6QTaVhxx/vjaR+gx7ZjaFgrgPRyZ3HCTbfwqYj6ZWEBHkCSUfcaymKPURaByukag==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-4.2.4.tgz", + "integrity": "sha512-5JWeMQYg81TgU4cG+OexAWdvDTs5JDdbEZx+Qr1iPbvo91QFGzjy0IkXAKaXUHqmKUJgSHK0ZxnCkgZpzkeNTA==", "dependencies": { "@smithy/is-array-buffer": "^3.0.0", - "@smithy/protocol-http": "^4.1.0", - "@smithy/types": "^3.3.0", + "@smithy/protocol-http": "^4.1.8", + "@smithy/types": "^3.7.2", "@smithy/util-hex-encoding": "^3.0.0", - "@smithy/util-middleware": "^3.0.3", + "@smithy/util-middleware": "^3.0.11", "@smithy/util-uri-escape": "^3.0.0", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" @@ -4213,15 +4829,16 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-3.2.0.tgz", - "integrity": "sha512-pDbtxs8WOhJLJSeaF/eAbPgXg4VVYFlRcL/zoNYA5WbG3wBL06CHtBSg53ppkttDpAJ/hdiede+xApip1CwSLw==", - "dependencies": { - "@smithy/middleware-endpoint": "^3.1.0", - "@smithy/middleware-stack": "^3.0.3", - "@smithy/protocol-http": "^4.1.0", - "@smithy/types": "^3.3.0", - "@smithy/util-stream": "^3.1.3", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-3.5.0.tgz", + "integrity": "sha512-Y8FeOa7gbDfCWf7njrkoRATPa5eNLUEjlJS5z5rXatYuGkCb80LbHcu8AQR8qgAZZaNHCLyo2N+pxPsV7l+ivg==", + "dependencies": { + "@smithy/core": "^2.5.5", + "@smithy/middleware-endpoint": "^3.2.5", + "@smithy/middleware-stack": "^3.0.11", + "@smithy/protocol-http": "^4.1.8", + "@smithy/types": "^3.7.2", + "@smithy/util-stream": "^3.3.2", "tslib": "^2.6.2" }, "engines": { @@ -4229,10 +4846,9 @@ } }, "node_modules/@smithy/types": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", - "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", - "license": "Apache-2.0", + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz", + "integrity": "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==", "dependencies": { "tslib": "^2.6.2" }, @@ -4241,13 +4857,12 @@ } }, "node_modules/@smithy/url-parser": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-3.0.3.tgz", - "integrity": "sha512-pw3VtZtX2rg+s6HMs6/+u9+hu6oY6U7IohGhVNnjbgKy86wcIsSZwgHrFR+t67Uyxvp4Xz3p3kGXXIpTNisq8A==", - "license": "Apache-2.0", + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-3.0.11.tgz", + "integrity": "sha512-TmlqXkSk8ZPhfc+SQutjmFr5FjC0av3GZP4B/10caK1SbRwe/v+Wzu/R6xEKxoNqL+8nY18s1byiy6HqPG37Aw==", "dependencies": { - "@smithy/querystring-parser": "^3.0.3", - "@smithy/types": "^3.3.0", + "@smithy/querystring-parser": "^3.0.11", + "@smithy/types": "^3.7.2", "tslib": "^2.6.2" } }, @@ -4312,13 +4927,13 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-3.0.15.tgz", - "integrity": "sha512-FZ4Psa3vjp8kOXcd3HJOiDPBCWtiilLl57r0cnNtq/Ga9RSDrM5ERL6xt+tO43+2af6Pn5Yp92x2n5vPuduNfg==", + "version": "3.0.30", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-3.0.30.tgz", + "integrity": "sha512-nLuGmgfcr0gzm64pqF2UT4SGWVG8UGviAdayDlVzJPNa6Z4lqvpDzdRXmLxtOdEjVlTOEdpZ9dd3ZMMu488mzg==", "dependencies": { - "@smithy/property-provider": "^3.1.3", - "@smithy/smithy-client": "^3.2.0", - "@smithy/types": "^3.3.0", + "@smithy/property-provider": "^3.1.11", + "@smithy/smithy-client": "^3.5.0", + "@smithy/types": "^3.7.2", "bowser": "^2.11.0", "tslib": "^2.6.2" }, @@ -4327,16 +4942,16 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-3.0.15.tgz", - "integrity": "sha512-KSyAAx2q6d0t6f/S4XB2+3+6aQacm3aLMhs9aLMqn18uYGUepbdssfogW5JQZpc6lXNBnp0tEnR5e9CEKmEd7A==", - "dependencies": { - "@smithy/config-resolver": "^3.0.5", - "@smithy/credential-provider-imds": "^3.2.0", - "@smithy/node-config-provider": "^3.1.4", - "@smithy/property-provider": "^3.1.3", - "@smithy/smithy-client": "^3.2.0", - "@smithy/types": "^3.3.0", + "version": "3.0.30", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-3.0.30.tgz", + "integrity": "sha512-OD63eWoH68vp75mYcfYyuVH+p7Li/mY4sYOROnauDrtObo1cS4uWfsy/zhOTW8F8ZPxQC1ZXZKVxoxvMGUv2Ow==", + "dependencies": { + "@smithy/config-resolver": "^3.0.13", + "@smithy/credential-provider-imds": "^3.2.8", + "@smithy/node-config-provider": "^3.1.12", + "@smithy/property-provider": "^3.1.11", + "@smithy/smithy-client": "^3.5.0", + "@smithy/types": "^3.7.2", "tslib": "^2.6.2" }, "engines": { @@ -4344,13 +4959,12 @@ } }, "node_modules/@smithy/util-endpoints": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-2.0.5.tgz", - "integrity": "sha512-ReQP0BWihIE68OAblC/WQmDD40Gx+QY1Ez8mTdFMXpmjfxSyz2fVQu3A4zXRfQU9sZXtewk3GmhfOHswvX+eNg==", - "license": "Apache-2.0", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-2.1.7.tgz", + "integrity": "sha512-tSfcqKcN/Oo2STEYCABVuKgJ76nyyr6skGl9t15hs+YaiU06sgMkN7QYjo0BbVw+KT26zok3IzbdSOksQ4YzVw==", "dependencies": { - "@smithy/node-config-provider": "^3.1.4", - "@smithy/types": "^3.3.0", + "@smithy/node-config-provider": "^3.1.12", + "@smithy/types": "^3.7.2", "tslib": "^2.6.2" }, "engines": { @@ -4370,12 +4984,11 @@ } }, "node_modules/@smithy/util-middleware": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-3.0.3.tgz", - "integrity": "sha512-l+StyYYK/eO3DlVPbU+4Bi06Jjal+PFLSMmlWM1BEwyLxZ3aKkf1ROnoIakfaA7mC6uw3ny7JBkau4Yc+5zfWw==", - "license": "Apache-2.0", + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-3.0.11.tgz", + "integrity": "sha512-dWpyc1e1R6VoXrwLoLDd57U1z6CwNSdkM69Ie4+6uYh2GC7Vg51Qtan7ITzczuVpqezdDTKJGJB95fFvvjU/ow==", "dependencies": { - "@smithy/types": "^3.3.0", + "@smithy/types": "^3.7.2", "tslib": "^2.6.2" }, "engines": { @@ -4383,13 +4996,12 @@ } }, "node_modules/@smithy/util-retry": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-3.0.3.tgz", - "integrity": "sha512-AFw+hjpbtVApzpNDhbjNG5NA3kyoMs7vx0gsgmlJF4s+yz1Zlepde7J58zpIRIsdjc+emhpAITxA88qLkPF26w==", - "license": "Apache-2.0", + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-3.0.11.tgz", + "integrity": "sha512-hJUC6W7A3DQgaee3Hp9ZFcOxVDZzmBIRBPlUAk8/fSOEl7pE/aX7Dci0JycNOnm9Mfr0KV2XjIlUOcGWXQUdVQ==", "dependencies": { - "@smithy/service-error-classification": "^3.0.3", - "@smithy/types": "^3.3.0", + "@smithy/service-error-classification": "^3.0.11", + "@smithy/types": "^3.7.2", "tslib": "^2.6.2" }, "engines": { @@ -4397,13 +5009,13 @@ } }, "node_modules/@smithy/util-stream": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-3.1.3.tgz", - "integrity": "sha512-FIv/bRhIlAxC0U7xM1BCnF2aDRPq0UaelqBHkM2lsCp26mcBbgI0tCVTv+jGdsQLUmAMybua/bjDsSu8RQHbmw==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-3.3.2.tgz", + "integrity": "sha512-sInAqdiVeisUGYAv/FrXpmJ0b4WTFmciTRqzhb7wVuem9BHvhIG7tpiYHLDWrl2stOokNZpTTGqz3mzB2qFwXg==", "dependencies": { - "@smithy/fetch-http-handler": "^3.2.4", - "@smithy/node-http-handler": "^3.1.4", - "@smithy/types": "^3.3.0", + "@smithy/fetch-http-handler": "^4.1.2", + "@smithy/node-http-handler": "^3.3.2", + "@smithy/types": "^3.7.2", "@smithy/util-base64": "^3.0.0", "@smithy/util-buffer-from": "^3.0.0", "@smithy/util-hex-encoding": "^3.0.0", @@ -4414,6 +5026,18 @@ "node": ">=16.0.0" } }, + "node_modules/@smithy/util-stream/node_modules/@smithy/fetch-http-handler": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-4.1.2.tgz", + "integrity": "sha512-R7rU7Ae3ItU4rC0c5mB2sP5mJNbCfoDc8I5XlYjIZnquyUwec7fEo78F6DA3SmgJgkU1qTMcZJuGblxZsl10ZA==", + "dependencies": { + "@smithy/protocol-http": "^4.1.8", + "@smithy/querystring-builder": "^3.0.11", + "@smithy/types": "^3.7.2", + "@smithy/util-base64": "^3.0.0", + "tslib": "^2.6.2" + } + }, "node_modules/@smithy/util-uri-escape": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-3.0.0.tgz", @@ -4439,6 +5063,19 @@ "node": ">=16.0.0" } }, + "node_modules/@smithy/util-waiter": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-3.2.0.tgz", + "integrity": "sha512-PpjSboaDUE6yl+1qlg3Si57++e84oXdWGbuFUSAciXsVfEZJJJupR2Nb0QuXHiunt2vGR+1PTizOMvnUPaG2Qg==", + "dependencies": { + "@smithy/abort-controller": "^3.1.9", + "@smithy/types": "^3.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/@socket.io/component-emitter": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", @@ -4723,6 +5360,21 @@ "@types/node": "*" } }, + "node_modules/@types/sinon": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.3.tgz", + "integrity": "sha512-j3uovdn8ewky9kRBG19bOwaZbexJu/XjtkHyjvUgt4xfPFz18dcORIMqnYh66Fx3Powhcr85NT5+er3+oViapw==", + "dev": true, + "dependencies": { + "@types/sinonjs__fake-timers": "*" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", + "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", + "dev": true + }, "node_modules/@types/stream-to-promise": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/@types/stream-to-promise/-/stream-to-promise-2.2.4.tgz", @@ -4738,6 +5390,11 @@ "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", "license": "MIT" }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==" + }, "node_modules/@types/yargs": { "version": "17.0.32", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", @@ -14393,6 +15050,14 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/mnemonist": { + "version": "0.38.3", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.38.3.tgz", + "integrity": "sha512-2K9QYubXx/NAjv4VLq1d1Ly8pWNC5L3BrixtdkyTegXWJIqY+zLNDhhX/A+ZwWt70tB1S8H4BE8FLYEFyNoOBw==", + "dependencies": { + "obliterator": "^1.6.1" + } + }, "node_modules/mocha": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.2.tgz", @@ -15743,6 +16408,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obliterator": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-1.6.1.tgz", + "integrity": "sha512-9WXswnqINnnhOG/5SLimUlzuU1hFJUc8zkwyD59Sd+dPOMf05PmnYG/d6Q7HZ+KmgkZJa1PxRso6QdM3sTNHig==" + }, "node_modules/on-exit-leak-free": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", @@ -17943,6 +18613,12 @@ "node": "^16.14.0 || >=18.0.0" } }, + "node_modules/sion": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/sion/-/sion-0.0.1.tgz", + "integrity": "sha512-SVAj7NHGRuAuIKamZhfnsR5uzi42ZzaWgTRfIk/yYVgvvPP+Wf8LBHqzchw9op5pdHVwQdCCUIcD03fK0oIe6Q==", + "dev": true + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", diff --git a/package.json b/package.json index d8ae5dc49..c28b55aef 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,6 @@ "license": "Apache-2.0", "dependencies": { "@aws-crypto/branch-keystore-node": "file:modules/branch-keystore-node", - "@aws-crypto/kdf-ctr-mode-node": "file:modules/kdf-ctr-mode-node", "@aws-crypto/cache-material": "file:modules/cache-material", "@aws-crypto/caching-materials-manager-browser": "file:modules/caching-materials-manager-browser", "@aws-crypto/caching-materials-manager-node": "file:modules/caching-materials-manager-node", @@ -81,6 +80,7 @@ "@aws-crypto/integration-browser": "file:modules/integration-browser", "@aws-crypto/integration-node": "file:modules/integration-node", "@aws-crypto/integration-vectors": "file:modules/integration-vectors", + "@aws-crypto/kdf-ctr-mode-node": "file:modules/kdf-ctr-mode-node", "@aws-crypto/kms-keyring": "file:modules/kms-keyring", "@aws-crypto/kms-keyring-browser": "file:modules/kms-keyring-browser", "@aws-crypto/kms-keyring-node": "file:modules/kms-keyring-node", @@ -100,7 +100,6 @@ ], "devDependencies": { "@aws-sdk/credential-provider-node": "^3.362.0", - "@aws-sdk/karma-credential-loader": "^3.38.0", "@aws-sdk/util-base64": "^3.374.0", "@jsdevtools/coverage-istanbul-loader": "^3.0.5", "@types/bn.js": "^5.1.0", @@ -109,7 +108,6 @@ "@types/from2": "^2.3.0", "@types/mocha": "^9.0.0", "@types/node": "^16.9.1", - "@types/uuid": "^10.0.0", "@typescript-eslint/eslint-plugin": "^4.31.1", "@typescript-eslint/parser": "^4.31.1", "aws-sdk": "^2.1409.0", @@ -136,9 +134,10 @@ "nyc": "^15.1.0", "prettier": "^2.0.4", "rimraf": "^6.0.1", + "sion": "^0.0.1", "source-map-support": "^0.5.19", "tslib": "^2.3.0", - "typescript": "^4.9.5", + "typescript": "^4.4.3", "verdaccio": "^5.13.1", "webpack": "^5.94.0", "webpack-cli": "^4.7.2" From 2383a745f916d062d9149a50e83070e9ff8cd701 Mon Sep 17 00:00:00 2001 From: seebees Date: Fri, 13 Dec 2024 17:11:11 -0800 Subject: [PATCH 03/25] more updates --- .../branch-keystore-node/src/kms_config.ts | 2 +- modules/kms-keyring/src/index.ts | 8 +- package-lock.json | 108 ++++++++++++++++++ package.json | 1 + 4 files changed, 117 insertions(+), 2 deletions(-) diff --git a/modules/branch-keystore-node/src/kms_config.ts b/modules/branch-keystore-node/src/kms_config.ts index 2004f9a26..d6640054f 100644 --- a/modules/branch-keystore-node/src/kms_config.ts +++ b/modules/branch-keystore-node/src/kms_config.ts @@ -9,7 +9,7 @@ import { constructArnInOtherRegion, mrkAwareAwsKmsKeyIdCompare, ParsedAwsKmsKeyArn, -} from '@aws-crypto/kms-keyring/src/arn_parsing' +} from '@aws-crypto/kms-keyring' import { needs, readOnlyProperty } from '@aws-crypto/material-management' //= aws-encryption-sdk-specification/framework/branch-key-store.md#aws-kms-configuration diff --git a/modules/kms-keyring/src/index.ts b/modules/kms-keyring/src/index.ts index d2e698623..45e13c7ef 100644 --- a/modules/kms-keyring/src/index.ts +++ b/modules/kms-keyring/src/index.ts @@ -2,7 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 export * from './kms_client_supplier' -export { getRegionFromIdentifier, parseAwsKmsKeyArn } from './arn_parsing' +export { + getRegionFromIdentifier, + parseAwsKmsKeyArn, + constructArnInOtherRegion, + mrkAwareAwsKmsKeyIdCompare, + ParsedAwsKmsKeyArn, +} from './arn_parsing' export * from './kms_keyring' export * from './kms_mrk_keyring' export * from './kms_mrk_discovery_keyring' diff --git a/package-lock.json b/package-lock.json index 2cc45ecd5..ad4ff6e15 100644 --- a/package-lock.json +++ b/package-lock.json @@ -80,6 +80,7 @@ "nyc": "^15.1.0", "prettier": "^2.0.4", "rimraf": "^6.0.1", + "sinon": "^19.0.2", "sion": "^0.0.1", "source-map-support": "^0.5.19", "tslib": "^2.3.0", @@ -90,6 +91,7 @@ } }, "modules/branch-keystore-node": { + "name": "@aws-crypto/branch-keystore-node", "version": "4.0.0", "license": "Apache-2.0", "dependencies": { @@ -509,6 +511,7 @@ } }, "modules/kdf-ctr-mode-node": { + "name": "@aws-crypto/kdf-ctr-mode-node", "version": "4.0.0", "license": "Apache-2.0", "dependencies": { @@ -4528,6 +4531,50 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.2.tgz", + "integrity": "sha512-v46t/fwnhejRSFTGqbpn9u+LQ9xJDse10gNnPgAcxgdoCDMXj/G2asWAC/8Qs+BAZDicX+MNZouXT1A7c83kVw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "lodash.get": "^4.4.2", + "type-detect": "^4.1.0" + } + }, + "node_modules/@sinonjs/samsam/node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", + "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", + "dev": true + }, "node_modules/@smithy/abort-controller": { "version": "3.1.9", "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-3.1.9.tgz", @@ -12944,6 +12991,12 @@ "integrity": "sha512-OYTthRfSh55WOItVqwpefPtNt2VdKsq5AnAK6apdtR6yCH8pr0CmSr710J0Mf+WdQy7K/OzMy7K2MgAfdQURDw==", "dev": true }, + "node_modules/just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", + "dev": true + }, "node_modules/jwa": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", @@ -14044,6 +14097,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -15411,6 +15470,28 @@ "dev": true, "license": "MIT" }, + "node_modules/nise": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/nise/-/nise-6.1.1.tgz", + "integrity": "sha512-aMSAzLVY7LyeM60gvBS423nBmIPP+Wy7St7hsb+8/fc1HmeoHJfLO8CKse4u3BtOZvQLJghYPI2i/1WZrEj5/g==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^13.0.1", + "@sinonjs/text-encoding": "^0.7.3", + "just-extend": "^6.2.0", + "path-to-regexp": "^8.1.0" + } + }, + "node_modules/nise/node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, "node_modules/node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", @@ -18613,6 +18694,33 @@ "node": "^16.14.0 || >=18.0.0" } }, + "node_modules/sinon": { + "version": "19.0.2", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-19.0.2.tgz", + "integrity": "sha512-euuToqM+PjO4UgXeLETsfQiuoyPXlqFezr6YZDFwHR3t4qaX0fZUe1MfPMznTL5f8BWrVS89KduLdMUsxFCO6g==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^13.0.2", + "@sinonjs/samsam": "^8.0.1", + "diff": "^7.0.0", + "nise": "^6.1.1", + "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon/node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/sion": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/sion/-/sion-0.0.1.tgz", diff --git a/package.json b/package.json index c28b55aef..4d5a0ed07 100644 --- a/package.json +++ b/package.json @@ -134,6 +134,7 @@ "nyc": "^15.1.0", "prettier": "^2.0.4", "rimraf": "^6.0.1", + "sinon": "^19.0.2", "sion": "^0.0.1", "source-map-support": "^0.5.19", "tslib": "^2.3.0", From aabbfed2bc5440d915280c559a70c60dc83e2d8b Mon Sep 17 00:00:00 2001 From: seebees Date: Fri, 13 Dec 2024 21:35:38 -0800 Subject: [PATCH 04/25] small updates to arn things --- .../branch-keystore-node/src/kms_config.ts | 11 ++- .../test/kms_config.test.ts | 95 +++++++++++-------- modules/kms-keyring/src/index.ts | 1 + 3 files changed, 67 insertions(+), 40 deletions(-) diff --git a/modules/branch-keystore-node/src/kms_config.ts b/modules/branch-keystore-node/src/kms_config.ts index d6640054f..613331d4d 100644 --- a/modules/branch-keystore-node/src/kms_config.ts +++ b/modules/branch-keystore-node/src/kms_config.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { + isMultiRegionAwsKmsArn, // getRegionFromIdentifier, parseAwsKmsKeyArn, } from '@aws-crypto/kms-keyring' @@ -78,13 +79,16 @@ export class KmsKeyConfig implements RegionalKmsConfig { //# that is a KMS ARN. constructor(config: KmsConfig) { readOnlyProperty(this, '_config', config) + /* Precondition: config must be a string or object */ + const configType = typeof config + needs(!!config && (configType === 'object' || 'string'), 'Config must be a `discovery` or an object.') if (config === 'discovery') { // Nothing to set } else if ('identifier' in config || 'mrkIdentifier' in config) { const arn = 'identifier' in config ? config.identifier : config.mrkIdentifier /* Precondition: ARN must be a string */ - needs(arn || typeof arn === 'string', 'ARN must be a string') + needs(typeof arn === 'string', 'ARN must be a string') //= aws-encryption-sdk-specification/framework/branch-key-store.md#aws-kms-configuration //# To be clear, an KMS ARN for a Multi-Region Key MAY be provided to the `KMS Key ARN` configuration, @@ -101,6 +105,7 @@ export class KmsKeyConfig implements RegionalKmsConfig { ) readOnlyProperty(this, '_parsedArn', parsedArn) + readOnlyProperty(this, '_arn', arn) } else if ('region' in config) { readOnlyProperty(this, '_mrkRegion', config.region) } else { @@ -170,7 +175,7 @@ export class KmsKeyConfig implements RegionalKmsConfig { //# For two ARNs to be compatible: //# If the [AWS KMS Configuration](#aws-kms-configuration) designates single region ARN compatibility, //# then two ARNs are compatible if they are exactly equal. - return this._arn == otherArn + return this._arn === otherArn } else if ('mrkIdentifier' in this._config) { //= aws-encryption-sdk-specification/framework/branch-key-store.md#aws-key-arn-compatibility //# If the [AWS KMS Configuration](#aws-kms-configuration) designates MRK ARN compatibility, @@ -207,7 +212,7 @@ export class KmsKeyConfig implements RegionalKmsConfig { //# If the KMS Configuration is MRDiscovery, `KeyId` MUST be the `kms-arn` attribute value of the AWS DDB response item, with the region replaced by the configured region. const parsedArn = parseAwsKmsKeyArn(otherArn) needs(parsedArn, 'KMS ARN from the keystore is not an ARN:' + otherArn) - return constructArnInOtherRegion(parsedArn, this._mrkRegion) + return isMultiRegionAwsKmsArn(parsedArn) ? constructArnInOtherRegion(parsedArn, this._mrkRegion) : otherArn } else if ( 'identifier' in this._config || 'mrkIdentifier' in this._config diff --git a/modules/branch-keystore-node/test/kms_config.test.ts b/modules/branch-keystore-node/test/kms_config.test.ts index 2f33f3be1..71f6378c3 100644 --- a/modules/branch-keystore-node/test/kms_config.test.ts +++ b/modules/branch-keystore-node/test/kms_config.test.ts @@ -2,11 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { expect } from 'chai' -import { - KmsKeyConfig, - RegionalKmsConfig, - KmsConfig, -} from '../src/kms_config' +import { KmsKeyConfig, RegionalKmsConfig, KmsConfig } from '../src/kms_config' function supplySrkKmsConfig(config: KmsConfig): KmsKeyConfig { return new KmsKeyConfig(config) @@ -29,9 +25,20 @@ export const WELL_FORMED_MRK_ALIAS_ARN = 'arn:aws:kms:us-west-2:123456789012:alias/mrk/my-mrk-alias' describe('Test KmsKeyConfig class', () => { + + it('Precondition: config must be a string or object', () => { + for (const config of [null, undefined, 0]) { + expect(() => supplySrkKmsConfig(config as any)).to.throw( + 'Config must be a `discovery` or an object.' + ) + } + }) it('Precondition: ARN must be a string', () => { for (const arn of [null, undefined, 0, {}]) { - expect(() => supplySrkKmsConfig(arn as any)).to.throw( + expect(() => supplySrkKmsConfig({identifier: arn} as any)).to.throw( + 'ARN must be a string' + ) + expect(() => supplySrkKmsConfig({mrkIdentifier: arn} as any)).to.throw( 'ARN must be a string' ) } @@ -67,20 +74,20 @@ describe('Test KmsKeyConfig class', () => { }) describe('Test getCompatibleArnArn', () => { - it('Returns the SRK', () => { - expect(config.getCompatibleArnArn(WELL_FORMED_SRK_ARN)).to.equal(WELL_FORMED_SRK_ARN) + expect(config.getCompatibleArnArn(WELL_FORMED_SRK_ARN)).to.equal( + WELL_FORMED_SRK_ARN + ) }) it('Throws for a non compatible value', () => { expect(() => config.getCompatibleArnArn(WELL_FORMED_MRK_ARN)).to.throw() }) - }) }) describe('Given a well formed MRK arn', () => { - const config = supplySrkKmsConfig({ identifier: WELL_FORMED_MRK_ARN }) + const config = supplySrkKmsConfig({ mrkIdentifier: WELL_FORMED_MRK_ARN }) it('Test getRegion', () => { expect((config as RegionalKmsConfig).getRegion()).equals('us-west-2') @@ -115,35 +122,33 @@ describe('Test KmsKeyConfig class', () => { }) describe('Test getCompatibleArnArn', () => { - it('Returns the MRK', () => { - expect(config.getCompatibleArnArn(WELL_FORMED_MRK_ARN)).to.equal(WELL_FORMED_MRK_ARN) + expect(config.getCompatibleArnArn(WELL_FORMED_MRK_ARN)).to.equal( + WELL_FORMED_MRK_ARN + ) }) it('Returns the configured MRK because it is the right region', () => { - expect(config.getCompatibleArnArn(WELL_FORMED_MRK_ARN_DIFFERENT_REGION)).to.equal(WELL_FORMED_MRK_ARN) + expect( + config.getCompatibleArnArn(WELL_FORMED_MRK_ARN_DIFFERENT_REGION) + ).to.equal(WELL_FORMED_MRK_ARN) }) it('Throws for a non compatible value', () => { expect(() => config.getCompatibleArnArn(WELL_FORMED_SRK_ARN)).to.throw() }) - }) }) describe('Given discovery configurations', () => { - it('Discovery is compatible with ARNs', () => { const config = supplySrkKmsConfig('discovery') - expect(config.isCompatibleWithArn(ONE_PART_ARN)).to.equal(true) expect(config.isCompatibleWithArn(WELL_FORMED_SRK_ARN)).to.equal(true) expect(config.isCompatibleWithArn(WELL_FORMED_MRK_ARN)).to.equal(true) }) - it('MRDiscovery is compatible with ARNs', () => { - const config = supplySrkKmsConfig({region: 'us-west-2'}) - expect(config.isCompatibleWithArn(ONE_PART_ARN)).to.equal(true) + const config = supplySrkKmsConfig({ region: 'us-west-2' }) expect(config.isCompatibleWithArn(WELL_FORMED_SRK_ARN)).to.equal(true) expect(config.isCompatibleWithArn(WELL_FORMED_MRK_ARN)).to.equal(true) }) @@ -151,60 +156,76 @@ describe('Test KmsKeyConfig class', () => { it('Discovery MUST be an ARN', () => { const config = supplySrkKmsConfig('discovery') expect(() => config.isCompatibleWithArn(MALFORMED_ARN)).to.throw() - expect(() => config.isCompatibleWithArn(WELL_FORMED_SRK_ALIAS_ARN)).to.throw() - expect(() => config.isCompatibleWithArn(WELL_FORMED_MRK_ALIAS_ARN)).to.throw() + expect(() => + config.isCompatibleWithArn(WELL_FORMED_SRK_ALIAS_ARN) + ).to.throw() + expect(() => + config.isCompatibleWithArn(WELL_FORMED_MRK_ALIAS_ARN) + ).to.throw() }) - it('MRDiscovery MUST be an ARN', () => { - const config = supplySrkKmsConfig({region: 'us-west-2'}) + const config = supplySrkKmsConfig({ region: 'us-west-2' }) expect(() => config.isCompatibleWithArn(MALFORMED_ARN)).to.throw() - expect(() => config.isCompatibleWithArn(WELL_FORMED_SRK_ALIAS_ARN)).to.throw() - expect(() => config.isCompatibleWithArn(WELL_FORMED_MRK_ALIAS_ARN)).to.throw() + expect(() => + config.isCompatibleWithArn(WELL_FORMED_SRK_ALIAS_ARN) + ).to.throw() + expect(() => + config.isCompatibleWithArn(WELL_FORMED_MRK_ALIAS_ARN) + ).to.throw() }) describe('Test getCompatibleArnArn for discovery', () => { const config = supplySrkKmsConfig('discovery') it('Returns the SRK', () => { - expect(config.getCompatibleArnArn(WELL_FORMED_SRK_ARN)).to.equal(WELL_FORMED_SRK_ARN) + expect(config.getCompatibleArnArn(WELL_FORMED_SRK_ARN)).to.equal( + WELL_FORMED_SRK_ARN + ) }) it('Returns the MRK', () => { - expect(config.getCompatibleArnArn(WELL_FORMED_MRK_ARN)).to.equal(WELL_FORMED_MRK_ARN) + expect(config.getCompatibleArnArn(WELL_FORMED_MRK_ARN)).to.equal( + WELL_FORMED_MRK_ARN + ) }) it('Returns the configured MRK because it is the right region', () => { - expect(config.getCompatibleArnArn(WELL_FORMED_MRK_ARN_DIFFERENT_REGION)).to.equal(WELL_FORMED_MRK_ARN_DIFFERENT_REGION) + expect( + config.getCompatibleArnArn(WELL_FORMED_MRK_ARN_DIFFERENT_REGION) + ).to.equal(WELL_FORMED_MRK_ARN_DIFFERENT_REGION) }) it('Throws for a non compatible value', () => { - expect(() => config.getCompatibleArnArn(WELL_FORMED_SRK_ARN)).to.throw() + expect(() => config.getCompatibleArnArn(ONE_PART_ARN)).to.throw() }) - }) describe('Test getCompatibleArnArn for MRDiscovery', () => { - const config = supplySrkKmsConfig({region: 'us-east-1'}) + const config = supplySrkKmsConfig({ region: 'us-east-1' }) it('Returns the SRK', () => { - expect(config.getCompatibleArnArn(WELL_FORMED_SRK_ARN)).to.equal(WELL_FORMED_SRK_ARN) + expect(config.getCompatibleArnArn(WELL_FORMED_SRK_ARN)).to.equal( + WELL_FORMED_SRK_ARN + ) }) it('Returns the MRK', () => { - expect(config.getCompatibleArnArn(WELL_FORMED_MRK_ARN)).to.equal(WELL_FORMED_MRK_ARN_DIFFERENT_REGION) + expect(config.getCompatibleArnArn(WELL_FORMED_MRK_ARN)).to.equal( + WELL_FORMED_MRK_ARN_DIFFERENT_REGION + ) }) it('Returns the configured MRK because it is the right region', () => { - expect(config.getCompatibleArnArn(WELL_FORMED_MRK_ARN_DIFFERENT_REGION)).to.equal(WELL_FORMED_MRK_ARN_DIFFERENT_REGION) + expect( + config.getCompatibleArnArn(WELL_FORMED_MRK_ARN_DIFFERENT_REGION) + ).to.equal(WELL_FORMED_MRK_ARN_DIFFERENT_REGION) }) it('Throws for a non compatible value', () => { - expect(() => config.getCompatibleArnArn(WELL_FORMED_SRK_ARN)).to.throw() + expect(() => config.getCompatibleArnArn(ONE_PART_ARN)).to.throw() }) - }) - }) //= aws-encryption-sdk-specification/framework/branch-key-store.md#aws-kms-configuration diff --git a/modules/kms-keyring/src/index.ts b/modules/kms-keyring/src/index.ts index 45e13c7ef..4b5cdede2 100644 --- a/modules/kms-keyring/src/index.ts +++ b/modules/kms-keyring/src/index.ts @@ -7,6 +7,7 @@ export { parseAwsKmsKeyArn, constructArnInOtherRegion, mrkAwareAwsKmsKeyIdCompare, + isMultiRegionAwsKmsArn, ParsedAwsKmsKeyArn, } from './arn_parsing' export * from './kms_keyring' From 57d4568fa8f67f4b357b29f2e6db0560c6dc04e7 Mon Sep 17 00:00:00 2001 From: seebees Date: Sun, 15 Dec 2024 11:18:11 -0800 Subject: [PATCH 05/25] updates for tests --- .../src/branch_keystore.ts | 2 +- .../branch-keystore-node/src/kms_config.ts | 7 ++-- .../test/branch_keystore.test.ts | 32 ++++++++----------- 3 files changed, 19 insertions(+), 22 deletions(-) diff --git a/modules/branch-keystore-node/src/branch_keystore.ts b/modules/branch-keystore-node/src/branch_keystore.ts index 0d901e9ae..b84e84a2c 100644 --- a/modules/branch-keystore-node/src/branch_keystore.ts +++ b/modules/branch-keystore-node/src/branch_keystore.ts @@ -82,7 +82,7 @@ export class BranchKeyStoreNode implements IBranchKeyStoreNode { 'Logical keystore name must be a string' ) - needs(kmsConfiguration, 'AWS KMS Configuration required') + /* Precondition: KMS Configuration must be provided. */ readOnlyProperty( this, 'kmsConfiguration', diff --git a/modules/branch-keystore-node/src/kms_config.ts b/modules/branch-keystore-node/src/kms_config.ts index 613331d4d..31997ca75 100644 --- a/modules/branch-keystore-node/src/kms_config.ts +++ b/modules/branch-keystore-node/src/kms_config.ts @@ -81,9 +81,10 @@ export class KmsKeyConfig implements RegionalKmsConfig { readOnlyProperty(this, '_config', config) /* Precondition: config must be a string or object */ const configType = typeof config - needs(!!config && (configType === 'object' || 'string'), 'Config must be a `discovery` or an object.') - if (config === 'discovery') { - // Nothing to set + needs(!!config && (configType === 'object' || configType === 'string'), 'Config must be a `discovery` or an object.') + if (configType === 'string') { + /* Precondition: Only `discovery` is a valid string value */ + needs(config === 'discovery', 'Unexpected config shape') } else if ('identifier' in config || 'mrkIdentifier' in config) { const arn = 'identifier' in config ? config.identifier : config.mrkIdentifier diff --git a/modules/branch-keystore-node/test/branch_keystore.test.ts b/modules/branch-keystore-node/test/branch_keystore.test.ts index 0f7d9cd1f..ae43e7257 100644 --- a/modules/branch-keystore-node/test/branch_keystore.test.ts +++ b/modules/branch-keystore-node/test/branch_keystore.test.ts @@ -93,11 +93,8 @@ describe('Test Branch keystore', () => { } }) - it('Precondition: KMS Configuration must be SRK', () => { - // all types of values - const badVals = [...falseyValues, ...truthyValues] - - for (const kmsConfiguration of badVals) { + it('Precondition: KMS Configuration must be provided.', () => { + for (const kmsConfiguration of [...falseyValues, ...truthyValues]) { expect( () => new BranchKeyStoreNode({ @@ -105,7 +102,7 @@ describe('Test Branch keystore', () => { logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, kmsConfiguration: kmsConfiguration as any, }) - ).to.throw('KMS Configuration must be SRK') + ).to.throw(/Unexpected config shape|Config must be a `discovery` or an object./) } }) @@ -193,11 +190,10 @@ describe('Test Branch keystore', () => { const kmsClient = new KMSClient({}) const ddbClient = new DynamoDBClient({}) expect(() => { - const kmsConfig = { identifier: KEY_ID } return new BranchKeyStoreNode({ storage: { ddbTableName: DDB_TABLE_NAME, ddbClient }, logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, - kmsConfiguration: kmsConfig, + kmsConfiguration: { identifier: KEY_ID}, keyManagement: { kmsClient }, }) }).to.throw( @@ -224,7 +220,7 @@ describe('Test Branch keystore', () => { it('Valid config', () => { const kmsClient = new KMSClient({}) const ddbClient = new DynamoDBClient({}) - const kmsConfig = { identifier: KEY_ID } + const kmsConfig = { identifier: KEY_ARN } const keyStore = new BranchKeyStoreNode({ storage: { ddbTableName: DDB_TABLE_NAME, ddbClient }, logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, @@ -236,13 +232,13 @@ describe('Test Branch keystore', () => { validate(keyStore.keyStoreId) && version(keyStore.keyStoreId) === 4 ).equals(true) // expect(keyStore.ddbTableName).equals(DDB_TABLE_NAME) - expect(keyStore.kmsConfiguration).equals(kmsConfig) + expect(keyStore.kmsConfiguration._config).equals(kmsConfig) }) it('Test valid config with no clients', () => { const kmsClient = new KMSClient({}) const ddbClient = new DynamoDBClient({}) - const kmsConfig = { identifier: KEY_ID } + const kmsConfig = { identifier: KEY_ARN } // test with no kms client supplied expect( @@ -390,7 +386,7 @@ describe('Test Branch keystore', () => { DDB_TABLE_NAME ) expect(test.logicalKeyStoreName).to.equal(LOGICAL_KEYSTORE_NAME) - expect(test.kmsConfiguration).to.equal(KMS_CONFIGURATION) + expect(test.kmsConfiguration._config).to.equal(KMS_CONFIGURATION) expect(test.kmsClient).to.equal(kmsClient) expect((test.storage as DynamoDBKeyStorage).ddbClient).to.equal( ddbClient @@ -407,7 +403,7 @@ describe('Test Branch keystore', () => { it('Test get active key', async () => { const kmsClient = new KMSClient({}) const ddbClient = new DynamoDBClient({}) - const kmsConfig = { identifier: KEY_ID } + const kmsConfig = { identifier: KEY_ARN } const keyStore = new BranchKeyStoreNode({ kmsConfiguration: kmsConfig, storage: { ddbTableName: DDB_TABLE_NAME, ddbClient: ddbClient }, @@ -443,7 +439,7 @@ describe('Test Branch keystore', () => { it('Test get branch key version', async () => { const kmsClient = new KMSClient({}) const ddbClient = new DynamoDBClient({}) - const kmsConfig = { identifier: KEY_ID } + const kmsConfig = { identifier: KEY_ARN } const keyStore = new BranchKeyStoreNode({ kmsConfiguration: kmsConfig, @@ -493,7 +489,7 @@ describe('Test Branch keystore', () => { it('Test get active key with incorrect kms key arn', async () => { const kmsClient = new KMSClient({}) const ddbClient = new DynamoDBClient({}) - const kmsConfig = { identifier: KEY_ID } + const kmsConfig = { identifier: KEY_ARN } const keyStore = new BranchKeyStoreNode({ kmsConfiguration: kmsConfig, @@ -513,7 +509,7 @@ describe('Test Branch keystore', () => { it('Test get active key with wrong logical keystore name', async () => { const kmsClient = new KMSClient({}) const ddbClient = new DynamoDBClient({}) - const kmsConfig = { identifier: KEY_ID } + const kmsConfig = { identifier: KEY_ARN } const keyStore = new BranchKeyStoreNode({ kmsConfiguration: kmsConfig, @@ -531,7 +527,7 @@ describe('Test Branch keystore', () => { it('Test get active key does not exist fails', async () => { const kmsClient = new KMSClient({}) const ddbClient = new DynamoDBClient({}) - const kmsConfig = { identifier: KEY_ID } + const kmsConfig = { identifier: KEY_ARN } const keyStore = new BranchKeyStoreNode({ kmsConfiguration: kmsConfig, @@ -549,7 +545,7 @@ describe('Test Branch keystore', () => { }) it('Test get active key with no clients', async () => { - const kmsConfig = { identifier: KEY_ID } + const kmsConfig = { identifier: KEY_ARN } const keyStore = new BranchKeyStoreNode({ kmsConfiguration: kmsConfig, logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, From e621e1b3661fc21fcb2f1c15f1e9f6739b79fff4 Mon Sep 17 00:00:00 2001 From: seebees Date: Sun, 15 Dec 2024 11:25:09 -0800 Subject: [PATCH 06/25] work around typescript --- .../branch-keystore-node/src/kms_config.ts | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/modules/branch-keystore-node/src/kms_config.ts b/modules/branch-keystore-node/src/kms_config.ts index 31997ca75..8272cac03 100644 --- a/modules/branch-keystore-node/src/kms_config.ts +++ b/modules/branch-keystore-node/src/kms_config.ts @@ -81,13 +81,23 @@ export class KmsKeyConfig implements RegionalKmsConfig { readOnlyProperty(this, '_config', config) /* Precondition: config must be a string or object */ const configType = typeof config - needs(!!config && (configType === 'object' || configType === 'string'), 'Config must be a `discovery` or an object.') + needs( + !!config && (configType === 'object' || configType === 'string'), + 'Config must be a `discovery` or an object.' + ) + const asdf = config + if (configType === 'string') { /* Precondition: Only `discovery` is a valid string value */ needs(config === 'discovery', 'Unexpected config shape') - } else if ('identifier' in config || 'mrkIdentifier' in config) { + } else if ( + 'identifier' in (config as any) || + 'mrkIdentifier' in (config as any) + ) { const arn = - 'identifier' in config ? config.identifier : config.mrkIdentifier + 'identifier' in (config as any) + ? (config as any).identifier + : (config as any).mrkIdentifier /* Precondition: ARN must be a string */ needs(typeof arn === 'string', 'ARN must be a string') @@ -107,8 +117,8 @@ export class KmsKeyConfig implements RegionalKmsConfig { readOnlyProperty(this, '_parsedArn', parsedArn) readOnlyProperty(this, '_arn', arn) - } else if ('region' in config) { - readOnlyProperty(this, '_mrkRegion', config.region) + } else if ('region' in (config as any)) { + readOnlyProperty(this, '_mrkRegion', (config as any).region) } else { needs(false, 'Unexpected config shape') } @@ -213,7 +223,9 @@ export class KmsKeyConfig implements RegionalKmsConfig { //# If the KMS Configuration is MRDiscovery, `KeyId` MUST be the `kms-arn` attribute value of the AWS DDB response item, with the region replaced by the configured region. const parsedArn = parseAwsKmsKeyArn(otherArn) needs(parsedArn, 'KMS ARN from the keystore is not an ARN:' + otherArn) - return isMultiRegionAwsKmsArn(parsedArn) ? constructArnInOtherRegion(parsedArn, this._mrkRegion) : otherArn + return isMultiRegionAwsKmsArn(parsedArn) + ? constructArnInOtherRegion(parsedArn, this._mrkRegion) + : otherArn } else if ( 'identifier' in this._config || 'mrkIdentifier' in this._config From 428e172bda9b77060cfd579281b261a6e791addf Mon Sep 17 00:00:00 2001 From: seebees Date: Sun, 15 Dec 2024 11:26:32 -0800 Subject: [PATCH 07/25] lint --- modules/branch-keystore-node/src/kms_config.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/branch-keystore-node/src/kms_config.ts b/modules/branch-keystore-node/src/kms_config.ts index 8272cac03..cd437e40b 100644 --- a/modules/branch-keystore-node/src/kms_config.ts +++ b/modules/branch-keystore-node/src/kms_config.ts @@ -85,7 +85,6 @@ export class KmsKeyConfig implements RegionalKmsConfig { !!config && (configType === 'object' || configType === 'string'), 'Config must be a `discovery` or an object.' ) - const asdf = config if (configType === 'string') { /* Precondition: Only `discovery` is a valid string value */ From bf93ce4cd7d8cb7c98777fa423e15876ea161fdd Mon Sep 17 00:00:00 2001 From: seebees Date: Sun, 15 Dec 2024 16:01:54 -0800 Subject: [PATCH 08/25] lint --- .../branch-keystore-node/src/branch_keystore.ts | 2 +- .../test/branch_keystore.test.ts | 6 ++++-- .../test/kms_config.test.ts | 5 ++--- .../src/caching_materials_manager_node.ts | 2 +- .../src/kms_hkeyring_node_helpers.ts | 4 ++-- modules/kms-keyring-node/test/fixtures.ts | 6 ++---- .../test/kms_hkeyring_node.constructor.test.ts | 6 ++---- .../test/kms_hkeyring_node.edk-order.test.ts | 15 +++++++++------ .../test/kms_hkeyring_node.ondecrypt.test.ts | 17 ++++++++++------- .../test/kms_hkeyring_node.onencrypt.test.ts | 15 +++++++++------ 10 files changed, 42 insertions(+), 36 deletions(-) diff --git a/modules/branch-keystore-node/src/branch_keystore.ts b/modules/branch-keystore-node/src/branch_keystore.ts index b84e84a2c..6f277b049 100644 --- a/modules/branch-keystore-node/src/branch_keystore.ts +++ b/modules/branch-keystore-node/src/branch_keystore.ts @@ -393,7 +393,7 @@ export class BranchKeyStoreNode implements IBranchKeyStoreNode { //# by calling [GetKeyStorageInfo](./key-store/key-storage.md#getkeystorageinfo). keystoreTableName: this.storage.getKeyStorageInfo().name, logicalKeyStoreName: this.logicalKeyStoreName, - grantTokens: !!this.grantTokens ? this.grantTokens.slice() : [], + grantTokens: this.grantTokens ? this.grantTokens.slice() : [], kmsConfiguration: this.kmsConfiguration._config, } } diff --git a/modules/branch-keystore-node/test/branch_keystore.test.ts b/modules/branch-keystore-node/test/branch_keystore.test.ts index ae43e7257..f9a4dcf29 100644 --- a/modules/branch-keystore-node/test/branch_keystore.test.ts +++ b/modules/branch-keystore-node/test/branch_keystore.test.ts @@ -102,7 +102,9 @@ describe('Test Branch keystore', () => { logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, kmsConfiguration: kmsConfiguration as any, }) - ).to.throw(/Unexpected config shape|Config must be a `discovery` or an object./) + ).to.throw( + /Unexpected config shape|Config must be a `discovery` or an object./ + ) } }) @@ -193,7 +195,7 @@ describe('Test Branch keystore', () => { return new BranchKeyStoreNode({ storage: { ddbTableName: DDB_TABLE_NAME, ddbClient }, logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, - kmsConfiguration: { identifier: KEY_ID}, + kmsConfiguration: { identifier: KEY_ID }, keyManagement: { kmsClient }, }) }).to.throw( diff --git a/modules/branch-keystore-node/test/kms_config.test.ts b/modules/branch-keystore-node/test/kms_config.test.ts index 71f6378c3..0dd77f435 100644 --- a/modules/branch-keystore-node/test/kms_config.test.ts +++ b/modules/branch-keystore-node/test/kms_config.test.ts @@ -25,7 +25,6 @@ export const WELL_FORMED_MRK_ALIAS_ARN = 'arn:aws:kms:us-west-2:123456789012:alias/mrk/my-mrk-alias' describe('Test KmsKeyConfig class', () => { - it('Precondition: config must be a string or object', () => { for (const config of [null, undefined, 0]) { expect(() => supplySrkKmsConfig(config as any)).to.throw( @@ -35,10 +34,10 @@ describe('Test KmsKeyConfig class', () => { }) it('Precondition: ARN must be a string', () => { for (const arn of [null, undefined, 0, {}]) { - expect(() => supplySrkKmsConfig({identifier: arn} as any)).to.throw( + expect(() => supplySrkKmsConfig({ identifier: arn } as any)).to.throw( 'ARN must be a string' ) - expect(() => supplySrkKmsConfig({mrkIdentifier: arn} as any)).to.throw( + expect(() => supplySrkKmsConfig({ mrkIdentifier: arn } as any)).to.throw( 'ARN must be a string' ) } diff --git a/modules/caching-materials-manager-node/src/caching_materials_manager_node.ts b/modules/caching-materials-manager-node/src/caching_materials_manager_node.ts index c7c8aab46..44fb05ae7 100644 --- a/modules/caching-materials-manager-node/src/caching_materials_manager_node.ts +++ b/modules/caching-materials-manager-node/src/caching_materials_manager_node.ts @@ -70,4 +70,4 @@ export class NodeCachingMaterialsManager //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#appendix-a-cache-entry-identifier-formulas //# When accessing the underlying cryptographic materials cache, //# the hierarchical keyring MUST use the formulas specified in this appendix -//# in order to compute the [cache entry identifier](../cryptographic-materials-cache.md#cache-identifier). \ No newline at end of file +//# in order to compute the [cache entry identifier](../cryptographic-materials-cache.md#cache-identifier). diff --git a/modules/kms-keyring-node/src/kms_hkeyring_node_helpers.ts b/modules/kms-keyring-node/src/kms_hkeyring_node_helpers.ts index 66ecdc28d..1f02a6719 100644 --- a/modules/kms-keyring-node/src/kms_hkeyring_node_helpers.ts +++ b/modules/kms-keyring-node/src/kms_hkeyring_node_helpers.ts @@ -86,14 +86,14 @@ export function getCacheEntryId( // get branch key id as a byte array const branchKeyIdAsBytes = stringToUtf8Bytes(branchKeyId) - var entryInfo + let entryInfo //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#resource-suffix //# The aforementioned 4 definitions ([Resource Identifier](#resource-identifier), //# [Scope Identifier](#scope-identifier), [Partition ID](#partition-id-1), and //# [Resource Suffix](#resource-suffix)) MUST be appended together with the null byte, 0x00, //# and the SHA384 of the result should be taken as the final cache identifier. - if (!!versionAsBytes) { + if (versionAsBytes) { //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#decryption-materials //# When the hierarchical keyring receives an OnDecrypt request, //# it MUST calculate the cache entry identifier as the diff --git a/modules/kms-keyring-node/test/fixtures.ts b/modules/kms-keyring-node/test/fixtures.ts index 7c3473184..16cd9935f 100644 --- a/modules/kms-keyring-node/test/fixtures.ts +++ b/modules/kms-keyring-node/test/fixtures.ts @@ -1,9 +1,7 @@ // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { - BranchKeyStoreNode, -} from '@aws-crypto/branch-keystore-node' +import { BranchKeyStoreNode } from '@aws-crypto/branch-keystore-node' import { AlgorithmSuiteIdentifier, EncryptionContext, @@ -26,7 +24,7 @@ export const TEST_ESDK_ALG_SUITE = new NodeAlgorithmSuite( ) export const TTL = 1 * 60000 * 10 export const KEYSTORE = new BranchKeyStoreNode({ - storage: {ddbTableName: DDB_TABLE_NAME}, + storage: { ddbTableName: DDB_TABLE_NAME }, logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, kmsConfiguration: { identifier: KEY_ARN }, }) diff --git a/modules/kms-keyring-node/test/kms_hkeyring_node.constructor.test.ts b/modules/kms-keyring-node/test/kms_hkeyring_node.constructor.test.ts index 63c37aca0..7c510ec33 100644 --- a/modules/kms-keyring-node/test/kms_hkeyring_node.constructor.test.ts +++ b/modules/kms-keyring-node/test/kms_hkeyring_node.constructor.test.ts @@ -1,9 +1,7 @@ // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { - BranchKeyStoreNode, -} from '@aws-crypto/branch-keystore-node' +import { BranchKeyStoreNode } from '@aws-crypto/branch-keystore-node' import { DDB_TABLE_NAME, LOGICAL_KEYSTORE_NAME, @@ -26,7 +24,7 @@ const branchKeyId = BRANCH_KEY_ID const cacheLimitTtl = TTL const maxCacheSize = 1000 const keyStore = new BranchKeyStoreNode({ - storage: {ddbTableName: DDB_TABLE_NAME}, + storage: { ddbTableName: DDB_TABLE_NAME }, logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, kmsConfiguration: { identifier: KEY_ARN }, }) diff --git a/modules/kms-keyring-node/test/kms_hkeyring_node.edk-order.test.ts b/modules/kms-keyring-node/test/kms_hkeyring_node.edk-order.test.ts index 75f0b6e82..d8d059818 100644 --- a/modules/kms-keyring-node/test/kms_hkeyring_node.edk-order.test.ts +++ b/modules/kms-keyring-node/test/kms_hkeyring_node.edk-order.test.ts @@ -35,7 +35,10 @@ import { testOnDecryptError, testOnEncrypt, } from './kms_hkeyring_node.test' -import { BranchKeyStoreNode, KeyStoreInfoOutput } from '@aws-crypto/branch-keystore-node' +import { + BranchKeyStoreNode, + KeyStoreInfoOutput, +} from '@aws-crypto/branch-keystore-node' chai.use(chaiAsPromised) // an edk that can't even be destructured according to any alg suite @@ -146,14 +149,14 @@ describe('KmsHierarchicalKeyRingNode: decrypt EDK order', () => { } }) - keyStore.getKeyStoreInfo.callsFake(function(): KeyStoreInfoOutput { + keyStore.getKeyStoreInfo.callsFake(function (): KeyStoreInfoOutput { return { - keystoreId: "keyStoreId", - keystoreTableName: "keystoreTableName", - logicalKeyStoreName: "logicalKeyStoreName", + keystoreId: 'keyStoreId', + keystoreTableName: 'keystoreTableName', + logicalKeyStoreName: 'logicalKeyStoreName', grantTokens: [], // This is not used by any tests - kmsConfiguration: null as any + kmsConfiguration: null as any, } }) }) diff --git a/modules/kms-keyring-node/test/kms_hkeyring_node.ondecrypt.test.ts b/modules/kms-keyring-node/test/kms_hkeyring_node.ondecrypt.test.ts index 2c9f5b809..620a2687f 100644 --- a/modules/kms-keyring-node/test/kms_hkeyring_node.ondecrypt.test.ts +++ b/modules/kms-keyring-node/test/kms_hkeyring_node.ondecrypt.test.ts @@ -34,7 +34,10 @@ import { expect } from 'chai' import Sinon from 'sinon' import { KMSClient } from '@aws-sdk/client-kms' import { DynamoDBClient } from '@aws-sdk/client-dynamodb' -import { BranchKeyStoreNode, KeyStoreInfoOutput } from '@aws-crypto/branch-keystore-node' +import { + BranchKeyStoreNode, + KeyStoreInfoOutput, +} from '@aws-crypto/branch-keystore-node' const branchKeyIdA = BRANCH_KEY_ID_A const branchKeyIdB = BRANCH_KEY_ID_B @@ -130,14 +133,14 @@ describe('KmsHierarchicalKeyRingNode: onDecrypt', () => { } }) - keyStore.getKeyStoreInfo.callsFake(function(): KeyStoreInfoOutput { + keyStore.getKeyStoreInfo.callsFake(function (): KeyStoreInfoOutput { return { - keystoreId: "keyStoreId", - keystoreTableName: "keystoreTableName", - logicalKeyStoreName: "logicalKeyStoreName", + keystoreId: 'keyStoreId', + keystoreTableName: 'keystoreTableName', + logicalKeyStoreName: 'logicalKeyStoreName', grantTokens: [], // This is not used by any tests - kmsConfiguration: null as any + kmsConfiguration: null as any, } }) }) @@ -595,7 +598,7 @@ describe('KmsHierarchicalKeyRingNode: onDecrypt', () => { it('CMC evictions occur due to capacity', async () => { const maxCacheSize = 1 - + const hkr = new KmsHierarchicalKeyRingNode({ branchKeyIdSupplier, keyStore, diff --git a/modules/kms-keyring-node/test/kms_hkeyring_node.onencrypt.test.ts b/modules/kms-keyring-node/test/kms_hkeyring_node.onencrypt.test.ts index a7407a4bc..641e8b23b 100644 --- a/modules/kms-keyring-node/test/kms_hkeyring_node.onencrypt.test.ts +++ b/modules/kms-keyring-node/test/kms_hkeyring_node.onencrypt.test.ts @@ -28,7 +28,10 @@ import { testOnEncrypt, testOnEncryptError, } from './kms_hkeyring_node.test' -import { BranchKeyStoreNode, KeyStoreInfoOutput } from '@aws-crypto/branch-keystore-node' +import { + BranchKeyStoreNode, + KeyStoreInfoOutput, +} from '@aws-crypto/branch-keystore-node' chai.use(chaiAsPromised) const branchKeyIdA = BRANCH_KEY_ID_A @@ -85,14 +88,14 @@ describe('KmsHierarchicalKeyRingNode: onEncrypt', () => { } }) - keyStore.getKeyStoreInfo.callsFake(function(): KeyStoreInfoOutput { + keyStore.getKeyStoreInfo.callsFake(function (): KeyStoreInfoOutput { return { - keystoreId: "keyStoreId", - keystoreTableName: "keystoreTableName", - logicalKeyStoreName: "logicalKeyStoreName", + keystoreId: 'keyStoreId', + keystoreTableName: 'keystoreTableName', + logicalKeyStoreName: 'logicalKeyStoreName', grantTokens: [], // This is not used by any tests - kmsConfiguration: null as any + kmsConfiguration: null as any, } }) }) From 913a73194bad56d24bcf400d32a82d38f8294918 Mon Sep 17 00:00:00 2001 From: seebees Date: Mon, 16 Dec 2024 13:34:18 -0800 Subject: [PATCH 09/25] Should be tests --- .../kms-keyring-node/test/kms_hkeyring_node.ondecrypt.test.ts | 1 + .../kms-keyring-node/test/kms_hkeyring_node.onencrypt.test.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/modules/kms-keyring-node/test/kms_hkeyring_node.ondecrypt.test.ts b/modules/kms-keyring-node/test/kms_hkeyring_node.ondecrypt.test.ts index 620a2687f..ac6584cfc 100644 --- a/modules/kms-keyring-node/test/kms_hkeyring_node.ondecrypt.test.ts +++ b/modules/kms-keyring-node/test/kms_hkeyring_node.ondecrypt.test.ts @@ -353,6 +353,7 @@ describe('KmsHierarchicalKeyRingNode: onDecrypt', () => { //# If a cache entry is found and the entry's TTL has not expired, the hierarchical keyring MUST use those branch key materials for key unwrapping. //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#ondecrypt + //= type=test //# If a cache entry is not found or the cache entry is expired, the hierarchical keyring //# MUST attempt to obtain the branch key materials by calling the backing branch key //# store specified in the [retrieve OnDecrypt branch key materials](#getitem-branch-keystore-ondecrypt) section. diff --git a/modules/kms-keyring-node/test/kms_hkeyring_node.onencrypt.test.ts b/modules/kms-keyring-node/test/kms_hkeyring_node.onencrypt.test.ts index 641e8b23b..24196ecff 100644 --- a/modules/kms-keyring-node/test/kms_hkeyring_node.onencrypt.test.ts +++ b/modules/kms-keyring-node/test/kms_hkeyring_node.onencrypt.test.ts @@ -241,6 +241,7 @@ describe('KmsHierarchicalKeyRingNode: onEncrypt', () => { //# If a cache entry is found and the entry's TTL has not expired, the hierarchical keyring MUST use those branch key materials for key wrapping. //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#onencrypt + //= type=test //# If a cache entry is not found or the cache entry is expired, the hierarchical keyring MUST attempt to obtain the branch key materials //# by querying the backing branch keystore specified in the [retrieve OnEncrypt branch key materials](#query-branch-keystore-onencrypt) section. //# If the keyring is not able to retrieve [branch key materials](../structures.md#branch-key-materials) From 3ea7fb81f86ed3e3fb1b5befd38d86a615e6d128 Mon Sep 17 00:00:00 2001 From: seebees Date: Mon, 16 Dec 2024 17:25:59 -0800 Subject: [PATCH 10/25] updates to compliance --- .../branch-keystore-node/src/kms_config.ts | 1 + modules/branch-keystore-node/src/types.ts | 15 ++++++---- .../test/branch_keystore.test.ts | 5 ++++ .../test/branch_keystore_helpers.test.ts | 28 +++++++++++-------- .../src/kms_hkeyring_node_helpers.ts | 4 ++- .../test/kms_hkeyring_node.ondecrypt.test.ts | 26 ----------------- .../test/kms_hkeyring_node.onencrypt.test.ts | 7 ++--- .../test/branch_key_id_supplier.test.ts | 6 ++-- 8 files changed, 41 insertions(+), 51 deletions(-) diff --git a/modules/branch-keystore-node/src/kms_config.ts b/modules/branch-keystore-node/src/kms_config.ts index cd437e40b..7b60fbb0c 100644 --- a/modules/branch-keystore-node/src/kms_config.ts +++ b/modules/branch-keystore-node/src/kms_config.ts @@ -183,6 +183,7 @@ export class KmsKeyConfig implements RegionalKmsConfig { } else if ('identifier' in this._config) { //= aws-encryption-sdk-specification/framework/branch-key-store.md#aws-key-arn-compatibility //# For two ARNs to be compatible: + //# //# If the [AWS KMS Configuration](#aws-kms-configuration) designates single region ARN compatibility, //# then two ARNs are compatible if they are exactly equal. return this._arn === otherArn diff --git a/modules/branch-keystore-node/src/types.ts b/modules/branch-keystore-node/src/types.ts index d46984262..28e57a902 100644 --- a/modules/branch-keystore-node/src/types.ts +++ b/modules/branch-keystore-node/src/types.ts @@ -54,7 +54,9 @@ export class ActiveHierarchicalSymmetricVersion { constructor(activeVersion: BranchKeyVersionType) { //= aws-encryption-sdk-specification/framework/branch-key-store.md#branch-key-materials-from-authenticated-encryption-context - //# If the `type` attribute start with `"branch:version:"` then the version string MUST be equal to this value. + //# If the `type` attribute is equal to `"branch:ACTIVE"` + //# then the authenticated encryption context MUST have a `version` attribute + //# and the version string is this value. needs( activeVersion.startsWith(BRANCH_KEY_TYPE_PREFIX), 'Unexpected branch key type.' @@ -78,7 +80,12 @@ export class HierarchicalSymmetricVersion { public declare readonly version: string constructor(type_field: BranchKeyVersionType) { - needs(type_field.startsWith(BRANCH_KEY_TYPE_PREFIX), '') + //= aws-encryption-sdk-specification/framework/branch-key-store.md#branch-key-materials-from-authenticated-encryption-context + //# If the `type` attribute start with `"branch:version:"` then the version string MUST be equal to this value. + needs( + type_field.startsWith(BRANCH_KEY_TYPE_PREFIX), + 'Type does not start with `branch:version:`' + ) readOnlyProperty( this, 'version', @@ -142,10 +149,6 @@ export class EncryptedHierarchicalKey { 'type', encryptionContext[TYPE_FIELD] == BRANCH_KEY_ACTIVE_TYPE ? new ActiveHierarchicalSymmetricVersion( - //= aws-encryption-sdk-specification/framework/branch-key-store.md#branch-key-materials-from-authenticated-encryption-context - //# If the `type` attribute is equal to `"branch:ACTIVE"` - //# then the authenticated encryption context MUST have a `version` attribute - //# and the version string is this value. encryptionContext[BRANCH_KEY_ACTIVE_VERSION_FIELD] ) : new HierarchicalSymmetricVersion(encryptionContext[TYPE_FIELD]) diff --git a/modules/branch-keystore-node/test/branch_keystore.test.ts b/modules/branch-keystore-node/test/branch_keystore.test.ts index f9a4dcf29..3f311804e 100644 --- a/modules/branch-keystore-node/test/branch_keystore.test.ts +++ b/modules/branch-keystore-node/test/branch_keystore.test.ts @@ -342,9 +342,12 @@ describe('Test Branch keystore', () => { //= type=test //# This name is cryptographically bound to all data stored in this table, //# and logically separates data between different tables. + //# //# The logical keystore name MUST be bound to every created key. + //# //# There needs to be a one to one mapping between DynamoDB Table Names and the Logical KeyStore Name. //# This value can be set to the DynamoDB table name itself, but does not need to. + //# //# Controlling this value independently enables restoring from DDB table backups //# even when the table name after restoration is not exactly the same. it('Null logical keystore name provided', () => { @@ -417,6 +420,7 @@ describe('Test Branch keystore', () => { //= aws-encryption-sdk-specification/framework/branch-key-store.md#getactivebranchkey //= type=test //# On invocation, the caller: + //# //# - MUST supply a `branch-key-id` await expect(keyStore.getActiveBranchKey('')).to.be.rejectedWith( 'MUST supply a string branch key id' @@ -454,6 +458,7 @@ describe('Test Branch keystore', () => { //= aws-encryption-sdk-specification/framework/branch-key-store.md#getbranchkeyversion //= type=test //# On invocation, the caller: + //# //# - MUST supply a `branch-key-id` //# - MUST supply a `branchKeyVersion` await expect( diff --git a/modules/branch-keystore-node/test/branch_keystore_helpers.test.ts b/modules/branch-keystore-node/test/branch_keystore_helpers.test.ts index 823ce3d0d..008604b28 100644 --- a/modules/branch-keystore-node/test/branch_keystore_helpers.test.ts +++ b/modules/branch-keystore-node/test/branch_keystore_helpers.test.ts @@ -574,17 +574,24 @@ describe('Test keystore helpers', () => { expect(versionedBranchKey).deep.equals(expectedVersionedBranchKey) }) }) + //= aws-encryption-sdk-specification/framework/branch-key-store.md#branch-key-materials-from-authenticated-encryption-context + //= type=test + //# - [Branch Key](./structures.md#branch-key) MUST be the [decrypted branch key material](#aws-kms-branch-key-decryption) + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#branch-key-materials-from-authenticated-encryption-context + //= type=test + //# - [Branch Key Id](./structures.md#branch-key-id) MUST be the `branch-key-id` - //= aws-encryption-sdk-specification/framework/branch-key-store.md#branch-key-materials-from-authenticated-encryption-context - //= type=test - //# To construct [branch key materials](./structures.md#branch-key-materials) from authenticated encryption context as follows: - //# - [Branch Key](./structures.md#branch-key) MUST be the [decrypted branch key material](#aws-kms-branch-key-decryption) - //# - [Branch Key Id](./structures.md#branch-key-id) MUST be the `branch-key-id` - //# - [Branch Key Version](./structures.md#branch-key-version) - //# The version string MUST start with `branch:version:`. - //# The remaining string encoded as UTF8 bytes MUST be the Branch Key version. - //# - [Encryption Context](./structures.md#encryption-context-3) MUST be constructed by - //# [Custom Encryption Context From Authenticated Encryption Context](#custom-encryption-context-from-authenticated-encryption-context) + //= aws-encryption-sdk-specification/framework/branch-key-store.md#branch-key-materials-from-authenticated-encryption-context + //= type=test + //# - [Branch Key Version](./structures.md#branch-key-version) + //# The version string MUST start with `branch:version:`. + //# The remaining string encoded as UTF8 bytes MUST be the Branch Key version. + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#branch-key-materials-from-authenticated-encryption-context + //= type=test + //# - [Encryption Context](./structures.md#encryption-context-3) MUST be constructed by + //# [Custom Encryption Context From Authenticated Encryption Context](#custom-encryption-context-from-authenticated-encryption-context) describe('Test constructBranchKeyMaterials', () => { const branchKey = Buffer.alloc(32) @@ -613,7 +620,6 @@ describe('Test keystore helpers', () => { //# If the `type` attribute is equal to `"branch:ACTIVE"` //# then the authenticated encryption context MUST have a `version` attribute //# and the version string is this value. - //# If the `type` attribute start with `"branch:version:"` then the version string MUST be equal to this value. expect(activeBranchKeyMaterials.branchKeyVersion).deep.equals( Buffer.from( activeAuthEc[BRANCH_KEY_ACTIVE_VERSION_FIELD].substring( diff --git a/modules/kms-keyring-node/src/kms_hkeyring_node_helpers.ts b/modules/kms-keyring-node/src/kms_hkeyring_node_helpers.ts index 1f02a6719..bdc42f0c3 100644 --- a/modules/kms-keyring-node/src/kms_hkeyring_node_helpers.ts +++ b/modules/kms-keyring-node/src/kms_hkeyring_node_helpers.ts @@ -260,8 +260,10 @@ export async function getBranchKeyMaterials( //# OnDecrypt MUST call the Keystore's [GetBranchKeyVersion](../branch-key-store.md#getbranchkeyversion) operation with the following inputs: branchKeyMaterials = branchKeyVersion ? await keyStore.getBranchKeyVersion(branchKeyId, branchKeyVersion) - : //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#query-branch-keystore-onencrypt + : // The complice needs a line + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#query-branch-keystore-onencrypt //# OnEncrypt MUST call the Keystore's [GetActiveBranchKey](../branch-key-store.md#getactivebranchkey) operation with the following inputs: + //# - the `branchKeyId` used in this operation await keyStore.getActiveBranchKey(branchKeyId) //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#query-branch-keystore-onencrypt diff --git a/modules/kms-keyring-node/test/kms_hkeyring_node.ondecrypt.test.ts b/modules/kms-keyring-node/test/kms_hkeyring_node.ondecrypt.test.ts index ac6584cfc..d870c7ec9 100644 --- a/modules/kms-keyring-node/test/kms_hkeyring_node.ondecrypt.test.ts +++ b/modules/kms-keyring-node/test/kms_hkeyring_node.ondecrypt.test.ts @@ -348,16 +348,6 @@ describe('KmsHierarchicalKeyRingNode: onDecrypt', () => { }) }) - //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#ondecrypt - //= type=test - //# If a cache entry is found and the entry's TTL has not expired, the hierarchical keyring MUST use those branch key materials for key unwrapping. - - //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#ondecrypt - //= type=test - //# If a cache entry is not found or the cache entry is expired, the hierarchical keyring - //# MUST attempt to obtain the branch key materials by calling the backing branch key - //# store specified in the [retrieve OnDecrypt branch key materials](#getitem-branch-keystore-ondecrypt) section. - //# If the keyring is not able to retrieve `branch key materials` from the backing keystore then OnDecrypt MUST fail. describe('Getting the branch key material', () => { it('Material X not already in the CMC or keystore, request material X', async () => { let hkr = new KmsHierarchicalKeyRingNode({ @@ -439,22 +429,6 @@ describe('KmsHierarchicalKeyRingNode: onDecrypt', () => { expect(kmsSendSpy.callCount).equals(1) }) - //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#getitem-branch-keystore-ondecrypt - //= type=test - //# The branch keystore persists [branch keys](#definitions) that are reused to derive unique data keys for key wrapping to - //# reduce the number of calls to AWS KMS through the use of the - //# [cryptographic materials cache](../cryptographic-materials-cache.md). - //# OnDecrypt MUST calculate the following values: - //# - Deserialize the UTF8-Decoded `branch-key-id` from the [key provider info](../structures.md#key-provider-information) of the [encrypted data key](../structures.md#encrypted-data-key) - //# and verify this is equal to the configured or supplied `branch-key-id`. - //# - Deserialize the UUID string representation of the `version` from the [encrypted data key](../structures.md#encrypted-data-key) [ciphertext](#ciphertext). - //# OnDecrypt MUST call the Keystore's [GetBranchKeyVersion](../branch-key-store.md#getbranchkeyversion) operation with the following inputs: - //# - The deserialized, UTF8-Decoded `branch-key-id` - //# - The deserialized UUID string representation of the `version` - //# If the Keystore's GetBranchKeyVersion operation succeeds - //# the keyring MUST put the returned branch key materials in the cache using the - //# formula defined in [Appendix A](#appendix-a-cache-entry-identifier-formulas). - //# Otherwise, OnDecrypt MUST fail. it('Material X already in CMC, request for Material X', async () => { const hkr = new KmsHierarchicalKeyRingNode({ branchKeyId: branchKeyIdA, diff --git a/modules/kms-keyring-node/test/kms_hkeyring_node.onencrypt.test.ts b/modules/kms-keyring-node/test/kms_hkeyring_node.onencrypt.test.ts index 24196ecff..ade975720 100644 --- a/modules/kms-keyring-node/test/kms_hkeyring_node.onencrypt.test.ts +++ b/modules/kms-keyring-node/test/kms_hkeyring_node.onencrypt.test.ts @@ -292,15 +292,14 @@ describe('KmsHierarchicalKeyRingNode: onEncrypt', () => { //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#query-branch-keystore-onencrypt //= type=test - //# The branch keystore persists [branch keys](#definitions) that are reused to derive unique data keys for envelope encryption to - //# reduce the number of calls to AWS KMS through the use of the - //# [cryptographic materials cache](../cryptographic-materials-cache.md). //# OnEncrypt MUST call the Keystore's [GetActiveBranchKey](../branch-key-store.md#getactivebranchkey) operation with the following inputs: //# - the `branchKeyId` used in this operation + + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#query-branch-keystore-onencrypt + //= type=test //# If the Keystore's GetActiveBranchKey operation succeeds //# the keyring MUST put the returned branch key materials in the cache using the //# formula defined in [Appendix A](#appendix-a-cache-entry-identifier-formulas). - //# Otherwise, OnEncrypt MUST fail. it('Material X already in CMC, request for Material X', async () => { const hkr = new KmsHierarchicalKeyRingNode({ branchKeyId: branchKeyIdA, diff --git a/modules/kms-keyring/test/branch_key_id_supplier.test.ts b/modules/kms-keyring/test/branch_key_id_supplier.test.ts index 10f399c12..a200dfa88 100644 --- a/modules/kms-keyring/test/branch_key_id_supplier.test.ts +++ b/modules/kms-keyring/test/branch_key_id_supplier.test.ts @@ -8,9 +8,9 @@ import { expect } from 'chai' describe('Branch key id supplier', () => { //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#branch-key-supplier //= type=test - //# The Branch Key Supplier is an interface containing the GetBranchKeyId - //# operation. This operation MUST take in an encryption context as input, and - //# return a branch key id (string) as output. + //# The Branch Key Supplier is an interface containing the `GetBranchKeyId` operation. + //# This operation MUST take in an encryption context as input, + //# and return a branch key id (string) as output. it('Can implement the interface', () => { class Example implements BranchKeyIdSupplier { getBranchKeyId(encryptionContext: EncryptionContext): string { From e925ca1e81ba55229df9d1bdfd3af784f73d0288 Mon Sep 17 00:00:00 2001 From: seebees Date: Wed, 8 Jan 2025 12:59:05 -0800 Subject: [PATCH 11/25] Lint --- .../test/branch_keystore_helpers.test.ts | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/modules/branch-keystore-node/test/branch_keystore_helpers.test.ts b/modules/branch-keystore-node/test/branch_keystore_helpers.test.ts index 008604b28..b8d42bb4b 100644 --- a/modules/branch-keystore-node/test/branch_keystore_helpers.test.ts +++ b/modules/branch-keystore-node/test/branch_keystore_helpers.test.ts @@ -574,24 +574,24 @@ describe('Test keystore helpers', () => { expect(versionedBranchKey).deep.equals(expectedVersionedBranchKey) }) }) - //= aws-encryption-sdk-specification/framework/branch-key-store.md#branch-key-materials-from-authenticated-encryption-context - //= type=test - //# - [Branch Key](./structures.md#branch-key) MUST be the [decrypted branch key material](#aws-kms-branch-key-decryption) - - //= aws-encryption-sdk-specification/framework/branch-key-store.md#branch-key-materials-from-authenticated-encryption-context - //= type=test - //# - [Branch Key Id](./structures.md#branch-key-id) MUST be the `branch-key-id` - - //= aws-encryption-sdk-specification/framework/branch-key-store.md#branch-key-materials-from-authenticated-encryption-context - //= type=test - //# - [Branch Key Version](./structures.md#branch-key-version) - //# The version string MUST start with `branch:version:`. - //# The remaining string encoded as UTF8 bytes MUST be the Branch Key version. - - //= aws-encryption-sdk-specification/framework/branch-key-store.md#branch-key-materials-from-authenticated-encryption-context - //= type=test - //# - [Encryption Context](./structures.md#encryption-context-3) MUST be constructed by - //# [Custom Encryption Context From Authenticated Encryption Context](#custom-encryption-context-from-authenticated-encryption-context) + //= aws-encryption-sdk-specification/framework/branch-key-store.md#branch-key-materials-from-authenticated-encryption-context + //= type=test + //# - [Branch Key](./structures.md#branch-key) MUST be the [decrypted branch key material](#aws-kms-branch-key-decryption) + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#branch-key-materials-from-authenticated-encryption-context + //= type=test + //# - [Branch Key Id](./structures.md#branch-key-id) MUST be the `branch-key-id` + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#branch-key-materials-from-authenticated-encryption-context + //= type=test + //# - [Branch Key Version](./structures.md#branch-key-version) + //# The version string MUST start with `branch:version:`. + //# The remaining string encoded as UTF8 bytes MUST be the Branch Key version. + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#branch-key-materials-from-authenticated-encryption-context + //= type=test + //# - [Encryption Context](./structures.md#encryption-context-3) MUST be constructed by + //# [Custom Encryption Context From Authenticated Encryption Context](#custom-encryption-context-from-authenticated-encryption-context) describe('Test constructBranchKeyMaterials', () => { const branchKey = Buffer.alloc(32) From 2c0dc18452bede36fe00bf73d9210859d0f0c263 Mon Sep 17 00:00:00 2001 From: seebees Date: Wed, 8 Jan 2025 15:20:14 -0800 Subject: [PATCH 12/25] Update tests --- .../test/branch_keystore.test.ts | 46 ++++++++++++++++++- .../kms_hkeyring_node.constructor.test.ts | 2 +- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/modules/branch-keystore-node/test/branch_keystore.test.ts b/modules/branch-keystore-node/test/branch_keystore.test.ts index 3f311804e..37b82ce83 100644 --- a/modules/branch-keystore-node/test/branch_keystore.test.ts +++ b/modules/branch-keystore-node/test/branch_keystore.test.ts @@ -291,7 +291,17 @@ describe('Test Branch keystore', () => { } }) - it('Postcondition: If unprovided, the DDB client is configured', async () => { + //= aws-encryption-sdk-specification/framework/branch-key-store.md#initialization + //= type=test + //# If a DDB client needs to be constructed and the AWS KMS Configuration is KMS Key ARN or KMS MRKey ARN, + //# a new DynamoDb client MUST be created with the region of the supplied KMS ARN. + //# + //# If a DDB client needs to be constructed and the AWS KMS Configuration is Discovery, + //# a new DynamoDb client MUST be created with the default configuration. + //# + //# If a DDB client needs to be constructed and the AWS KMS Configuration is MRDiscovery, + //# a new DynamoDb client MUST be created with the region configured in the MRDiscovery. + it('If unprovided, the DDB client is configured', async () => { for (const ddbClient of falseyValues) { const { storage } = new BranchKeyStoreNode({ storage: { @@ -307,6 +317,40 @@ describe('Test Branch keystore', () => { await (storage as DynamoDBKeyStorage).ddbClient.config.region() ).to.equal(getRegionFromIdentifier(KEY_ARN)) } + + const mrkDiscovery = new BranchKeyStoreNode({ + storage: { + ddbTableName: DDB_TABLE_NAME + }, + logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, + kmsConfiguration: { region: 'foo' }, + }) + + expect( + await (mrkDiscovery.storage as DynamoDBKeyStorage).ddbClient.config.region() + ).to.equal('foo') + + const discovery = new BranchKeyStoreNode({ + storage: { + ddbTableName: DDB_TABLE_NAME + }, + logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, + kmsConfiguration: 'discovery', + }) + + expect( + await (discovery.storage as DynamoDBKeyStorage).ddbClient.config.region() + ).to.equal('foo') + }) + + it('Precondition: Only `discovery` is a valid string value', async () => { + expect(() => new BranchKeyStoreNode({ + storage: { + ddbTableName: DDB_TABLE_NAME + }, + logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, + kmsConfiguration: 'not discovery' as any, + })).to.throw('Unexpected config shape') }) it('Postcondition: If unprovided, the KMS client is configured', async () => { diff --git a/modules/kms-keyring-node/test/kms_hkeyring_node.constructor.test.ts b/modules/kms-keyring-node/test/kms_hkeyring_node.constructor.test.ts index 7c510ec33..bc4a0a22d 100644 --- a/modules/kms-keyring-node/test/kms_hkeyring_node.constructor.test.ts +++ b/modules/kms-keyring-node/test/kms_hkeyring_node.constructor.test.ts @@ -266,7 +266,7 @@ describe('KmsHierarchicalKeyRingNode: constructor', () => { //= type=test //# If no max cache size is provided, the cryptographic materials cache MUST be configured to a //# max cache size of 1000. - it('Postcondition: The max cache size is initialized', () => { + it('The max cache size is initialized', () => { expect( new KmsHierarchicalKeyRingNode({ branchKeyId, From ebd510ddc4dc346009c5e7ca59918ab8a9177663 Mon Sep 17 00:00:00 2001 From: seebees Date: Wed, 8 Jan 2025 20:57:37 -0800 Subject: [PATCH 13/25] Adding test information --- .../src/branch_keystore.ts | 5 +- .../branch-keystore-node/src/kms_config.ts | 6 +- .../test/branch_keystore.test.ts | 135 ++++++++++++++++-- 3 files changed, 127 insertions(+), 19 deletions(-) diff --git a/modules/branch-keystore-node/src/branch_keystore.ts b/modules/branch-keystore-node/src/branch_keystore.ts index 6f277b049..13b8822f0 100644 --- a/modules/branch-keystore-node/src/branch_keystore.ts +++ b/modules/branch-keystore-node/src/branch_keystore.ts @@ -31,15 +31,18 @@ import { DynamoDBKeyStorage } from './dynamodb_key_storage' interface IBranchKeyStoreNode { //= aws-encryption-sdk-specification/framework/branch-key-store.md#operations + //= type=implication //# - [GetActiveBranchKey](#getactivebranchkey) getActiveBranchKey(branchKeyId: string): Promise //= aws-encryption-sdk-specification/framework/branch-key-store.md#operations + //= type=implication //# - [GetBranchKeyVersion](#getbranchkeyversion) getBranchKeyVersion( branchKeyId: string, branchKeyVersion: string ): Promise //= aws-encryption-sdk-specification/framework/branch-key-store.md#operations + //= type=implication //# - [GetKeyStoreInfo](#getkeystoreinfo) getKeyStoreInfo(): KeyStoreInfoOutput } @@ -106,7 +109,7 @@ export class BranchKeyStoreNode implements IBranchKeyStoreNode { //= aws-encryption-sdk-specification/framework/branch-key-store.md#initialization //# If [Storage](#storage) is configured with [KeyStorage](#keystorage) //# then this MUST be the configured [KeyStorage interface](./key-store/key-storage.md#interface). - this.storage + this.storage = storage } else { //= aws-encryption-sdk-specification/framework/branch-key-store.md#initialization //# If [Storage](#storage) is not configured with [KeyStorage](#keystorage) diff --git a/modules/branch-keystore-node/src/kms_config.ts b/modules/branch-keystore-node/src/kms_config.ts index 7b60fbb0c..a8282121f 100644 --- a/modules/branch-keystore-node/src/kms_config.ts +++ b/modules/branch-keystore-node/src/kms_config.ts @@ -56,7 +56,7 @@ export interface RegionalKmsConfig { * this method tells the user the config's region * @returns the region */ - getRegion(): string + getRegion(): string | undefined /** * this method tells the user if the config is compatible with an arn @@ -125,7 +125,7 @@ export class KmsKeyConfig implements RegionalKmsConfig { Object.freeze(this) } - getRegion(): string { + getRegion(): string | undefined { if (this._config === 'discovery') { //= aws-encryption-sdk-specification/framework/branch-key-store.md#initialization //# If a DDB client needs to be constructed and the AWS KMS Configuration is Discovery, @@ -134,7 +134,7 @@ export class KmsKeyConfig implements RegionalKmsConfig { //= aws-encryption-sdk-specification/framework/branch-key-store.md#initialization //# If AWS KMS client needs to be constructed and the AWS KMS Configuration is Discovery, //# a new AWS KMS client MUST be created with the default configuration. - return '' + return undefined } else if ( 'identifier' in this._config || 'mrkIdentifier' in this._config diff --git a/modules/branch-keystore-node/test/branch_keystore.test.ts b/modules/branch-keystore-node/test/branch_keystore.test.ts index 37b82ce83..ba59b2ff5 100644 --- a/modules/branch-keystore-node/test/branch_keystore.test.ts +++ b/modules/branch-keystore-node/test/branch_keystore.test.ts @@ -33,6 +33,7 @@ import { } from './fixtures' import { BRANCH_KEY_ACTIVE_TYPE, + KMS_CLIENT_USER_AGENT, PARTITION_KEY, SORT_KEY, } from '../src/constants' @@ -233,7 +234,6 @@ describe('Test Branch keystore', () => { expect( validate(keyStore.keyStoreId) && version(keyStore.keyStoreId) === 4 ).equals(true) - // expect(keyStore.ddbTableName).equals(DDB_TABLE_NAME) expect(keyStore.kmsConfiguration._config).equals(kmsConfig) }) @@ -313,48 +313,97 @@ describe('Test Branch keystore', () => { }) expect(storage instanceof DynamoDBKeyStorage).to.equals(true) + //= aws-encryption-sdk-specification/framework/branch-key-store.md#initialization + //= type=test + //# This constructed [default key storage](./key-store/default-key-storage.md#initialization) + //# MUST be configured with either the [Table Name](#table-name) or the [DynamoDBTable](#dynamodbtable) table name + //# depending on which one is configured. + expect((storage as DynamoDBKeyStorage).ddbTableName).to.equal( + DDB_TABLE_NAME + ) + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#initialization + //= type=test + //# This constructed [default key storage](./key-store/default-key-storage.md#initialization) + //# MUST be configured with either the [DynamoDb Client](#dynamodb-client), the DDB client in the [DynamoDBTable](#dynamodbtable) + //# or a constructed DDB client depending on what is configured. + expect((storage as DynamoDBKeyStorage).logicalKeyStoreName).to.equal( + LOGICAL_KEYSTORE_NAME + ) + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#initialization + //= type=test + //# This constructed [default key storage](./key-store/default-key-storage.md#initialization) + //# MUST be configured with either the [DynamoDb Client](#dynamodb-client), the DDB client in the [DynamoDBTable](#dynamodbtable) + //# or a constructed DDB client depending on what is configured. + expect( + (storage as DynamoDBKeyStorage).ddbClient instanceof DynamoDBClient + ).to.equal(true) + expect( await (storage as DynamoDBKeyStorage).ddbClient.config.region() ).to.equal(getRegionFromIdentifier(KEY_ARN)) + + expect(storage instanceof DynamoDBKeyStorage).to.equals(true) } const mrkDiscovery = new BranchKeyStoreNode({ storage: { - ddbTableName: DDB_TABLE_NAME + ddbTableName: DDB_TABLE_NAME, }, logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, kmsConfiguration: { region: 'foo' }, }) expect( - await (mrkDiscovery.storage as DynamoDBKeyStorage).ddbClient.config.region() - ).to.equal('foo') + await ( + mrkDiscovery.storage as DynamoDBKeyStorage + ).ddbClient.config.region() + ).to.equal('foo') const discovery = new BranchKeyStoreNode({ storage: { - ddbTableName: DDB_TABLE_NAME + ddbTableName: DDB_TABLE_NAME, }, logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, kmsConfiguration: 'discovery', }) expect( - await (discovery.storage as DynamoDBKeyStorage).ddbClient.config.region() - ).to.equal('foo') + await ( + discovery.storage as DynamoDBKeyStorage + ).ddbClient.config.region() + ).to.not.equal('') }) it('Precondition: Only `discovery` is a valid string value', async () => { - expect(() => new BranchKeyStoreNode({ - storage: { - ddbTableName: DDB_TABLE_NAME - }, - logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, - kmsConfiguration: 'not discovery' as any, - })).to.throw('Unexpected config shape') + expect( + () => + new BranchKeyStoreNode({ + storage: { + ddbTableName: DDB_TABLE_NAME, + }, + logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, + kmsConfiguration: 'not discovery' as any, + }) + ).to.throw('Unexpected config shape') }) + //= aws-encryption-sdk-specification/framework/branch-key-store.md#initialization + //= type=test + //# If a DDB client needs to be constructed and the AWS KMS Configuration is KMS Key ARN or KMS MRKey ARN, + //# a new DynamoDb client MUST be created with the region of the supplied KMS ARN. + //# + //# If a DDB client needs to be constructed and the AWS KMS Configuration is Discovery, + //# a new DynamoDb client MUST be created with the default configuration. + //# + //# If a DDB client needs to be constructed and the AWS KMS Configuration is MRDiscovery, + //# a new DynamoDb client MUST be created with the region configured in the MRDiscovery. it('Postcondition: If unprovided, the KMS client is configured', async () => { for (const kmsClient of falseyValues) { + //= aws-encryption-sdk-specification/framework/branch-key-store.md#initialization + //= type=test + //# If no AWS KMS client is provided one MUST be constructed. const { kmsClient: client } = new BranchKeyStoreNode({ storage: { ddbTableName: DDB_TABLE_NAME }, logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, @@ -365,7 +414,36 @@ describe('Test Branch keystore', () => { expect(await client.config.region()).to.equal( getRegionFromIdentifier(KEY_ARN) ) + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#initialization + //= type=test + //# On initialization the KeyStore SHOULD + //# append a user agent string to the AWS KMS SDK Client with + //# the value `aws-kms-hierarchy`. + expect(client.config.customUserAgent).to.deep.equal([ + [KMS_CLIENT_USER_AGENT], + ]) } + + const mrkDiscovery = new BranchKeyStoreNode({ + storage: { + ddbTableName: DDB_TABLE_NAME, + }, + logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, + kmsConfiguration: { region: 'foo' }, + }) + + expect(await mrkDiscovery.kmsClient.config.region()).to.equal('foo') + + const discovery = new BranchKeyStoreNode({ + storage: { + ddbTableName: DDB_TABLE_NAME, + }, + logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, + kmsConfiguration: 'discovery', + }) + + expect(await discovery.kmsClient.config.region()).to.not.equal('') }) //= aws-encryption-sdk-specification/framework/branch-key-store.md#table-name @@ -414,6 +492,10 @@ describe('Test Branch keystore', () => { expect(Object.isFrozen(BRANCH_KEYSTORE)).equals(true) }) + it('Storage is immutable', () => { + expect(Object.isFrozen(BRANCH_KEYSTORE.storage)).equals(true) + }) + it('Attributes are correct', () => { const kmsClient = new KMSClient({ region: getRegionFromIdentifier(KEY_ARN), @@ -434,6 +516,19 @@ describe('Test Branch keystore', () => { expect((test.storage as DynamoDBKeyStorage).ddbTableName).to.equal( DDB_TABLE_NAME ) + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#logical-keystore-name + //= type=test + //# This name is cryptographically bound to all data stored in this table, + //# and logically separates data between different tables. + //# + //# The logical keystore name MUST be bound to every created key. + //# + //# There needs to be a one to one mapping between DynamoDB Table Names and the Logical KeyStore Name. + //# This value can be set to the DynamoDB table name itself, but does not need to. + //# + //# Controlling this value independently enables restoring from DDB table backups + //# even when the table name after restoration is not exactly the same. expect(test.logicalKeyStoreName).to.equal(LOGICAL_KEYSTORE_NAME) expect(test.kmsConfiguration._config).to.equal(KMS_CONFIGURATION) expect(test.kmsClient).to.equal(kmsClient) @@ -479,10 +574,20 @@ describe('Test Branch keystore', () => { ) const branchKeyMaterials = await keyStore.getActiveBranchKey(BRANCH_KEY_ID) - // expect(branchKeyMaterials.branchKeyIdentifier).equals(BRANCH_KEY_ID) + //= aws-encryption-sdk-specification/framework/branch-key-store.md#getactivebranchkey + //= type=test + //# GetActiveBranchKey MUST verify that the returned EncryptedHierarchicalKey MUST have the requested `branch-key-id`. + expect(branchKeyMaterials.branchKeyIdentifier).equals(BRANCH_KEY_ID) + //= aws-encryption-sdk-specification/framework/branch-key-store.md#getactivebranchkey + //= type=test + //# GetActiveBranchKey MUST verify that the returned EncryptedHierarchicalKey is an ActiveHierarchicalSymmetricVersion. expect(branchKeyMaterials.branchKeyVersion).deep.equals( BRANCH_KEY_ACTIVE_VERSION_UTF8_BYTES ) + + //= aws-encryption-sdk-specification/framework/branch-key-store.md#getactivebranchkey + //= type=test + //# This operation MUST return the constructed [branch key materials](./structures.md#branch-key-materials). expect(branchKeyMaterials.branchKey().length).equals(32) }) From 68d86cd087ac738434539354b08d55ef17a1d26f Mon Sep 17 00:00:00 2001 From: seebees Date: Wed, 8 Jan 2025 20:59:57 -0800 Subject: [PATCH 14/25] Update spec version --- aws-encryption-sdk-specification | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws-encryption-sdk-specification b/aws-encryption-sdk-specification index bd9acf0c5..6fd8f886f 160000 --- a/aws-encryption-sdk-specification +++ b/aws-encryption-sdk-specification @@ -1 +1 @@ -Subproject commit bd9acf0c59509a6e9809de35b7396450fa01d3a5 +Subproject commit 6fd8f886f708afeb89bcfb2a618ca57bb2bd48cd From 27eb26ad183aea2ffc267b7376ace5bb5898177c Mon Sep 17 00:00:00 2001 From: seebees Date: Thu, 9 Jan 2025 09:01:11 -0800 Subject: [PATCH 15/25] lint --- .../kms-keyring-node/test/kms_hkeyring_node.onencrypt.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/kms-keyring-node/test/kms_hkeyring_node.onencrypt.test.ts b/modules/kms-keyring-node/test/kms_hkeyring_node.onencrypt.test.ts index ade975720..436ece415 100644 --- a/modules/kms-keyring-node/test/kms_hkeyring_node.onencrypt.test.ts +++ b/modules/kms-keyring-node/test/kms_hkeyring_node.onencrypt.test.ts @@ -294,7 +294,7 @@ describe('KmsHierarchicalKeyRingNode: onEncrypt', () => { //= type=test //# OnEncrypt MUST call the Keystore's [GetActiveBranchKey](../branch-key-store.md#getactivebranchkey) operation with the following inputs: //# - the `branchKeyId` used in this operation - + //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#query-branch-keystore-onencrypt //= type=test //# If the Keystore's GetActiveBranchKey operation succeeds From ff3aad6bd8d8dfb7cf23aeae6c7c5f6142771572 Mon Sep 17 00:00:00 2001 From: seebees Date: Thu, 9 Jan 2025 10:05:03 -0800 Subject: [PATCH 16/25] More test vectors --- modules/kdf-ctr-mode-node/src/kdfctr.ts | 16 +- modules/kdf-ctr-mode-node/test/fixtures.ts | 458 ++++++++++++++++++ modules/kdf-ctr-mode-node/test/kdfctr.test.ts | 65 +-- modules/kdf-ctr-mode-node/test/testvectors.ts | 218 --------- 4 files changed, 469 insertions(+), 288 deletions(-) create mode 100644 modules/kdf-ctr-mode-node/test/fixtures.ts delete mode 100644 modules/kdf-ctr-mode-node/test/testvectors.ts diff --git a/modules/kdf-ctr-mode-node/src/kdfctr.ts b/modules/kdf-ctr-mode-node/src/kdfctr.ts index a0f5d937f..722755bc9 100644 --- a/modules/kdf-ctr-mode-node/src/kdfctr.ts +++ b/modules/kdf-ctr-mode-node/src/kdfctr.ts @@ -14,12 +14,10 @@ import { uInt32BE } from '@aws-crypto/serialize' const SEPARATION_INDICATOR = Buffer.from([0x00]) const COUNTER_START_VALUE = 1 export const INT32_MAX_LIMIT = 2147483647 -const SUPPORTED_IKM_LENGTHS = [32] -const SUPPORTED_NONCE_LENGTHS = [16] const SUPPORTED_DERIVED_KEY_LENGTHS = [32] -const SUPPORTED_DIGEST_ALGORITHMS = ['sha256'] +const SUPPORTED_DIGEST_ALGORITHMS = ['sha256', 'sha384'] -export type SupportedDigestAlgorithms = 'sha256' +export type SupportedDigestAlgorithms = 'sha256' | 'sha384' export type SupportedDerivedKeyLengths = 32 interface KdfCtrInput { @@ -37,18 +35,8 @@ export function kdfCounterMode({ purpose, expectedLength, }: KdfCtrInput): Buffer { - /* Precondition: the ikm must be 32 bytes long */ - needs( - SUPPORTED_IKM_LENGTHS.includes(ikm.length), - `Unsupported IKM length ${ikm.length}` - ) /* Precondition: the nonce is required */ needs(nonce, 'The nonce must be provided') - /* Precondition: the nonce must be 16 bytes long */ - needs( - SUPPORTED_NONCE_LENGTHS.includes(nonce.length), - `Unsupported nonce length ${nonce.length}` - ) /* Precondition: the expected length must be 32 bytes */ /* Precondition: the expected length * 8 must be under the max 32-bit signed integer */ needs( diff --git a/modules/kdf-ctr-mode-node/test/fixtures.ts b/modules/kdf-ctr-mode-node/test/fixtures.ts new file mode 100644 index 000000000..1f09698c5 --- /dev/null +++ b/modules/kdf-ctr-mode-node/test/fixtures.ts @@ -0,0 +1,458 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// based on https://github.com/aws/aws-cryptographic-material-providers-library/blob/main/AwsCryptographyPrimitives/test/TestKDF_TestVectors.dfy + +import { createHash } from 'crypto' +import { + SupportedDigestAlgorithms, + SupportedDerivedKeyLengths, +} from '../src/kdfctr' +import { expect } from 'chai' + +interface InternalTestVector { + name: string + hash: SupportedDigestAlgorithms + ikm: Buffer + info: Buffer + L: number + okm: Buffer +} + +interface TestVector extends InternalTestVector { + purpose: Buffer + L: SupportedDerivedKeyLengths +} + +const PURPOSE = Buffer.from('aws-kms-hierarchy', 'utf-8') + +const b1: InternalTestVector = { + name: 'B.1 Test Case 1', + hash: 'sha256', + ikm: Buffer.from([ + 226, 4, 214, 212, 102, 170, 213, 7, 255, 175, 109, 109, 171, 10, 91, 38, 21, + 44, 158, 33, 231, 100, 55, 4, 100, 227, 96, 200, 251, 199, 101, 198, + ]), + info: Buffer.from([ + 123, 3, 185, 141, 159, 148, 184, 153, 229, 145, 243, 239, 38, 75, 113, 177, + 147, 251, 167, 4, 60, 126, 149, 60, 222, 35, 188, 83, 132, 188, 26, 98, 147, + 88, 1, 21, 250, 227, 73, 95, 216, 69, 218, 219, 208, 43, 214, 69, 92, 244, + 141, 15, 98, 179, 62, 98, 54, 74, 58, 128, + ]), + L: 32, + okm: Buffer.from([ + 119, 13, 250, 182, 166, 164, 164, 190, 224, 37, 127, 243, 53, 33, 63, 120, + 216, 40, 123, 79, 213, 55, 213, 193, 255, 250, 149, 105, 16, 231, 199, 121, + ]), +} + +const b2: InternalTestVector = { + name: 'B.2 Test Case 2', + hash: 'sha256', + ikm: Buffer.from([ + 174, 238, 202, 96, 246, 137, 164, 65, 177, 59, 12, 188, 212, 65, 216, 45, + 240, 207, 135, 218, 194, 54, 41, 13, 236, 232, 147, 29, 248, 215, 3, 23, + ]), + info: Buffer.from([ + 88, 142, 192, 65, 229, 115, 59, 112, 49, 33, 44, 85, 56, 239, 228, 246, 170, + 250, 76, 218, 139, 146, 93, 38, 31, 90, 38, 136, 240, 7, 179, 172, 36, 14, + 225, 41, 145, 231, 123, 140, 184, 83, 134, 120, 97, 89, 102, 22, 74, 129, + 135, 43, 209, 207, 203, 251, 57, 164, 244, 80, + ]), + L: 32, + okm: Buffer.from([ + 62, 129, 214, 17, 60, 238, 60, 82, 158, 206, 223, 248, 154, 105, 153, 206, + 37, 182, 24, 193, 94, 225, 209, 157, 69, 203, 55, 106, 28, 142, 35, 116, + ]), +} + +const b3: InternalTestVector = { + name: 'B.3 Test Case 3', + hash: 'sha256', + ikm: Buffer.from([ + 149, 200, 247, 110, 17, 54, 126, 181, 85, 38, 162, 179, 147, 174, 144, 101, + 131, 209, 203, 221, 71, 150, 33, 70, 245, 6, 204, 124, 172, 18, 244, 100, + ]), + info: Buffer.from([ + 202, 214, 14, 144, 75, 158, 156, 139, 254, 180, 168, 26, 127, 103, 211, 189, + 220, 192, 94, 100, 37, 88, 112, 64, 55, 112, 243, 83, 58, 230, 221, 99, 76, + 234, 165, 108, 83, 230, 136, 189, 19, 122, 230, 1, 137, 53, 243, 75, 159, + 176, 132, 234, 72, 228, 198, 136, 246, 187, 179, 136, + ]), + L: 32, + okm: Buffer.from([ + 202, 250, 92, 160, 63, 95, 190, 42, 36, 32, 4, 171, 203, 211, 222, 16, 89, + 199, 64, 123, 30, 229, 121, 37, 81, 36, 175, 24, 155, 224, 181, 86, + ]), +} + +const b4: InternalTestVector = { + name: 'B.4 Test Case 4', + hash: 'sha256', + ikm: Buffer.from([ + 77, 5, 57, 31, 214, 251, 30, 41, 46, 120, 171, 150, 25, 177, 183, 42, 125, + 99, 238, 89, 215, 67, 93, 215, 24, 151, 185, 255, 126, 231, 174, 112, + ]), + info: Buffer.from([ + 240, 120, 230, 249, 183, 248, 45, 100, 85, 79, 166, 182, 4, 200, 8, 241, + 155, 31, 106, 214, 114, 125, 183, 170, 111, 28, 134, 105, 78, 16, 75, 82, + 86, 200, 180, 3, 153, 25, 100, 100, 129, 215, 234, 36, 82, 199, 44, 23, 163, + 232, 215, 211, 145, 98, 133, 70, 10, 165, 235, 129, + ]), + L: 32, + okm: Buffer.from([ + 107, 22, 232, 245, 59, 131, 26, 165, 232, 107, 249, 122, 92, 79, 163, 125, + 8, 155, 193, 114, 218, 90, 30, 127, 102, 45, 212, 165, 149, 51, 154, 183, + ]), +} + +const b5: InternalTestVector = { + name: 'B.5 Test Case 5', + hash: 'sha256', + ikm: Buffer.from([ + 15, 104, 168, 47, 241, 103, 22, 52, 204, 145, 54, 197, 100, 169, 224, 42, + 118, 118, 33, 221, 116, 161, 191, 92, 36, 18, 155, 128, 130, 20, 183, 82, + ]), + info: Buffer.from([ + 100, 133, 153, 128, 156, 44, 78, 124, 106, 94, 108, 68, 159, 0, 49, 235, + 245, 92, 54, 97, 168, 149, 180, 77, 176, 87, 46, 232, 128, 131, 177, 244, + 177, 38, 2, 170, 85, 252, 29, 241, 80, 166, 90, 109, 110, 237, 160, 170, + 121, 164, 52, 161, 3, 155, 145, 181, 165, 143, 199, 241, + ]), + L: 32, + okm: Buffer.from([ + 226, 151, 100, 15, 119, 104, 72, 93, 74, 110, 124, 254, 36, 95, 139, 250, + 132, 112, 13, 153, 118, 38, 146, 234, 26, 66, 92, 204, 2, 117, 232, 245, + ]), +} + +const b6: InternalTestVector = { + name: 'B.6 Test Case 6', + hash: 'sha384', + ikm: Buffer.from([ + 130, 44, 118, 74, 27, 17, 112, 133, 193, 15, 14, 104, 152, 20, 210, 191, + 189, 155, 67, 40, 127, 26, 140, 117, 215, 149, 169, 131, 26, 40, 97, 132, + 200, 88, 111, 53, 119, 182, 229, 187, 206, 22, 55, 146, 94, 4, 252, 71, + ]), + info: Buffer.from([ + 175, 52, 97, 16, 185, 65, 177, 29, 33, 137, 49, 108, 159, 194, 185, 244, 33, + 55, 117, 165, 215, 54, 141, 53, 65, 38, 120, 162, 143, 205, 3, 176, 127, 5, + 73, 102, 110, 253, 243, 12, 128, 240, 171, 85, 99, 114, 10, 86, 239, 97, + 106, 19, 187, 143, 119, 128, 3, 111, 192, 142, + ]), + L: 32, + okm: Buffer.from([ + 224, 174, 35, 92, 184, 35, 128, 82, 123, 231, 105, 52, 166, 150, 34, 57, + 109, 144, 231, 191, 167, 226, 210, 149, 228, 55, 91, 206, 224, 209, 177, 1, + ]), +} + +const b7: InternalTestVector = { + name: 'B.7 Test Case 7', + hash: 'sha384', + ikm: Buffer.from([ + 52, 14, 33, 45, 117, 142, 131, 204, 91, 137, 228, 181, 106, 134, 238, 140, + 150, 49, 174, 78, 75, 186, 236, 21, 172, 9, 94, 164, 64, 123, 199, 182, 52, + 173, 99, 13, 208, 190, 133, 169, 28, 8, 168, 199, 225, 225, 3, 11, + ]), + info: Buffer.from([ + 60, 213, 86, 26, 209, 47, 173, 252, 228, 8, 224, 65, 128, 175, 206, 227, + 139, 131, 21, 107, 158, 75, 224, 119, 156, 79, 13, 185, 226, 107, 254, 92, + 205, 67, 225, 89, 33, 151, 124, 210, 107, 29, 184, 40, 139, 128, 8, 158, + 183, 209, 187, 215, 245, 158, 16, 17, 179, 225, 139, 81, + ]), + L: 32, + okm: Buffer.from([ + 5, 250, 87, 123, 112, 129, 33, 14, 124, 157, 230, 157, 176, 61, 124, 32, 38, + 207, 68, 105, 169, 11, 250, 41, 241, 194, 193, 8, 24, 212, 99, 224, + ]), +} + +const b8: InternalTestVector = { + name: 'B.8 Test Case 8', + hash: 'sha384', + ikm: Buffer.from([ + 0, 161, 45, 60, 228, 255, 117, 166, 227, 15, 65, 243, 85, 124, 130, 106, + 241, 50, 107, 99, 2, 244, 206, 136, 123, 173, 61, 51, 23, 165, 72, 200, 192, + 58, 5, 114, 132, 220, 195, 141, 139, 198, 144, 189, 74, 86, 95, 71, + ]), + info: Buffer.from([ + 36, 197, 192, 178, 200, 16, 223, 160, 142, 53, 215, 254, 235, 184, 199, 142, + 12, 215, 38, 201, 46, 205, 66, 217, 23, 16, 19, 115, 140, 162, 83, 26, 148, + 127, 82, 60, 55, 246, 76, 219, 4, 48, 91, 217, 105, 209, 214, 249, 236, 212, + 100, 5, 210, 130, 128, 249, 104, 80, 11, 167, + ]), + L: 32, + okm: Buffer.from([ + 174, 243, 213, 124, 141, 167, 217, 88, 44, 93, 28, 98, 214, 182, 72, 150, + 218, 155, 27, 14, 64, 18, 164, 76, 220, 61, 207, 75, 112, 173, 108, 102, + ]), +} + +const b9: InternalTestVector = { + name: 'B.9 Test Case 9', + hash: 'sha384', + ikm: Buffer.from([ + 0, 0, 217, 183, 236, 111, 190, 253, 242, 86, 253, 104, 34, 11, 82, 5, 172, + 101, 162, 0, 17, 69, 17, 140, 80, 186, 107, 101, 112, 50, 25, 139, 139, 124, + 227, 178, 247, 6, 138, 120, 13, 193, 124, 34, 69, 154, 242, 183, + ]), + info: Buffer.from([ + 216, 87, 84, 28, 98, 184, 87, 86, 220, 115, 222, 125, 194, 216, 111, 93, 94, + 139, 40, 51, 139, 176, 169, 69, 181, 196, 253, 124, 129, 247, 25, 97, 185, + 112, 93, 61, 21, 59, 25, 25, 93, 0, 59, 116, 33, 32, 104, 237, 16, 249, 108, + 83, 67, 134, 83, 8, 122, 1, 82, 207, + ]), + L: 20, + okm: Buffer.from([ + 121, 62, 241, 19, 249, 99, 151, 171, 0, 49, 234, 160, 250, 167, 119, 193, 7, + 231, 208, 60, + ]), +} + +const b10: InternalTestVector = { + name: 'B.10 Test Case 10', + hash: 'sha384', + ikm: Buffer.from([ + 79, 61, 116, 77, 62, 68, 158, 6, 39, 191, 68, 152, 116, 56, 40, 248, 110, + 99, 143, 96, 98, 10, 126, 212, 167, 201, 181, 176, 115, 105, 28, 158, 201, + 71, 40, 197, 136, 34, 232, 39, 240, 246, 204, 248, 109, 188, 28, 174, + ]), + info: Buffer.from([ + 48, 31, 238, 178, 94, 108, 168, 80, 62, 205, 130, 31, 29, 55, 135, 174, 191, + 179, 208, 236, 81, 139, 179, 17, 116, 245, 32, 155, 42, 193, 242, 142, 211, + 230, 152, 115, 107, 173, 16, 161, 142, 60, 189, 181, 220, 39, 187, 209, 45, + 5, 139, 54, 219, 8, 146, 249, 207, 208, 131, 0, + ]), + L: 20, + okm: Buffer.from([ + 133, 239, 149, 5, 178, 48, 86, 94, 204, 242, 166, 74, 179, 222, 83, 229, + 169, 28, 123, 81, + ]), +} + +const c1: TestVector = { + name: 'C.1 Test Case 1', + hash: 'sha256', + ikm: Buffer.from([ + 125, 201, 189, 252, 37, 52, 4, 124, 254, 99, 233, 235, 41, 123, 119, 82, 81, + 73, 237, 125, 74, 252, 233, 198, 68, 15, 53, 14, 97, 239, 62, 208, + ]), + info: Buffer.from([ + 119, 218, 233, 62, 104, 155, 88, 29, 62, 6, 235, 1, 200, 211, 186, 2, + ]), + purpose: PURPOSE, + L: 32, + okm: Buffer.from([ + 188, 232, 152, 114, 85, 137, 174, 192, 143, 152, 52, 179, 184, 15, 220, 63, + 241, 115, 144, 126, 85, 116, 231, 41, 253, 206, 18, 124, 247, 109, 183, 204, + ]), +} + +const c2: TestVector = { + name: 'C.2 Test Case 2', + hash: 'sha256', + ikm: Buffer.from([ + 80, 22, 113, 23, 118, 68, 10, 32, 75, 169, 199, 192, 255, 220, 214, 60, 182, + 1, 126, 147, 171, 233, 110, 177, 35, 145, 217, 129, 30, 9, 80, 159, + ]), + info: Buffer.from([ + 210, 241, 192, 158, 103, 66, 27, 35, 143, 66, 168, 189, 82, 171, 211, 252, + ]), + purpose: PURPOSE, + L: 32, + okm: Buffer.from([ + 54, 206, 174, 72, 237, 133, 85, 156, 93, 53, 120, 152, 118, 82, 89, 33, 114, + 98, 204, 236, 138, 57, 162, 118, 85, 92, 199, 232, 240, 252, 92, 97, + ]), +} + +const c3: TestVector = { + name: 'C.3 Test Case 3', + hash: 'sha256', + ikm: Buffer.from([ + 57, 90, 16, 46, 83, 54, 189, 241, 27, 242, 237, 236, 246, 66, 54, 226, 74, + 112, 79, 156, 208, 13, 148, 71, 117, 211, 139, 57, 73, 69, 122, 236, + ]), + info: Buffer.from([ + 51, 15, 183, 124, 82, 229, 249, 86, 117, 148, 237, 162, 27, 243, 173, 108, + ]), + purpose: PURPOSE, + L: 32, + okm: Buffer.from([ + 22, 55, 236, 141, 159, 163, 250, 236, 86, 47, 225, 103, 156, 225, 228, 146, + 166, 45, 244, 39, 136, 163, 205, 200, 116, 193, 20, 147, 112, 254, 210, 194, + ]), +} + +const c4: TestVector = { + name: 'C.4 Test Case 4', + hash: 'sha256', + ikm: Buffer.from([ + 152, 192, 25, 223, 239, 154, 175, 67, 237, 250, 184, 146, 228, 243, 227, 1, + 128, 247, 228, 152, 142, 131, 149, 41, 60, 70, 244, 58, 166, 234, 86, 189, + ]), + info: Buffer.from([ + 243, 160, 102, 127, 219, 137, 115, 38, 187, 216, 48, 80, 151, 168, 148, 71, + ]), + purpose: PURPOSE, + L: 32, + okm: Buffer.from([ + 191, 112, 86, 234, 220, 233, 122, 154, 100, 188, 230, 238, 239, 155, 54, 32, + 97, 35, 51, 160, 121, 235, 42, 64, 145, 105, 15, 153, 162, 89, 9, 156, + ]), +} + +const c5: TestVector = { + name: 'C.5 Test Case 5', + hash: 'sha256', + ikm: Buffer.from([ + 166, 236, 116, 51, 140, 189, 192, 175, 42, 154, 51, 26, 208, 149, 76, 159, + 174, 162, 207, 4, 108, 232, 196, 240, 12, 57, 228, 155, 97, 75, 42, 66, + ]), + info: Buffer.from([ + 236, 169, 233, 45, 43, 25, 122, 243, 152, 191, 154, 55, 45, 134, 159, 220, + ]), + purpose: PURPOSE, + L: 32, + okm: Buffer.from([ + 156, 11, 20, 251, 100, 227, 163, 161, 30, 45, 242, 2, 248, 246, 44, 11, 88, + 132, 189, 175, 95, 96, 61, 44, 98, 160, 212, 136, 140, 222, 57, 151, + ]), +} + +// These test cases are a grab bag of configuration +// These sizes come from asking Dafny to generate tests for this code. +// The highlights were strange lengths of buffers, and expected key length. +// Dafny found that the interesting `L` was 2147482807. +// Way to huge to be practical, and JS limits us to 32... +// So these focus on + +const t1: TestVector = { + name: '256 with empty info/nonce and purpose', + hash: 'sha256', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([]), + purpose: Buffer.from([]), + L: 32, + okm: Buffer.from([ + 131, 81, 40, 59, 15, 210, 28, 71, 12, 87, 94, 236, 126, 33, 233, 108, 95, + 149, 146, 127, 4, 11, 241, 34, 234, 165, 22, 19, 114, 10, 141, 187, + ]), +} + +const t2: TestVector = { + name: '256 with empty purpose', + hash: 'sha256', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([0, 0, 0]), + purpose: Buffer.from([]), + L: 32, + okm: Buffer.from([ + 162, 137, 147, 225, 21, 174, 44, 20, 121, 29, 226, 152, 146, 76, 195, 64, + 174, 149, 122, 67, 111, 209, 67, 10, 87, 112, 245, 42, 14, 133, 247, 131, + ]), +} + +const t3: TestVector = { + name: '256 with empty info/nonce', + hash: 'sha256', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([]), + purpose: Buffer.from([0, 0, 0]), + L: 32, + okm: Buffer.from([ + 162, 137, 147, 225, 21, 174, 44, 20, 121, 29, 226, 152, 146, 76, 195, 64, + 174, 149, 122, 67, 111, 209, 67, 10, 87, 112, 245, 42, 14, 133, 247, 131, + ]), +} + +const t4: TestVector = { + name: '384 with empty info/nonce and purpose', + hash: 'sha384', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([]), + purpose: Buffer.from([]), + L: 32, + okm: Buffer.from([ + 77, 201, 182, 192, 122, 4, 68, 27, 246, 185, 15, 31, 62, 179, 227, 246, 181, + 129, 114, 189, 185, 1, 61, 118, 67, 14, 77, 219, 40, 6, 123, 205, + ]), +} + +const t5: TestVector = { + name: '384 with empty purpose', + hash: 'sha384', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([0, 0, 0]), + purpose: Buffer.from([]), + L: 32, + okm: Buffer.from([ + 169, 94, 115, 125, 197, 199, 196, 79, 166, 30, 97, 233, 173, 170, 235, 242, + 13, 176, 204, 52, 135, 119, 6, 237, 104, 18, 186, 179, 255, 64, 13, 78, + ]), +} + +const t6: TestVector = { + name: '384 with empty info/nonce', + hash: 'sha384', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([]), + purpose: Buffer.from([0, 0, 0]), + L: 32, + okm: Buffer.from([ + 169, 94, 115, 125, 197, 199, 196, 79, 166, 30, 97, 233, 173, 170, 235, 242, + 13, 176, 204, 52, 135, 119, 6, 237, 104, 18, 186, 179, 255, 64, 13, 78, + ]), +} + +export const rawTestVectors = [b1, b2, b3, b4, b5, b6, b7, b8, b9, b10] +export const testVectors = [c1, c2, c3, c4, c5, t1, t2, t3, t4, t5, t6] + +export const vectorOkmDigest = Buffer.from([ + 183, 87, 35, 163, 83, 138, 133, 68, 69, 201, 220, 32, 102, 155, 232, 241, 122, + 8, 244, 48, 26, 60, 130, 7, 161, 138, 47, 123, 44, 65, 31, 56, +]) + +export const testVectorDigest = () => + createHash('sha256') + .update( + Buffer.from( + [...rawTestVectors, ...testVectors].flatMap((v) => [...v.okm]) + ) + ) + .digest() + +// It is complicated to check every byte of ever vector +// This takes all the okm values and digests them. +// This way it is easy for us to say +// that JS and Dafny are testing the same test vectors. +// I only use okm because there is a deterministic relationship +// between all inputs and the okm. +it('Make sure that the test vectors are the same', () => { + expect(testVectorDigest()).to.deep.equal(vectorOkmDigest) +}) diff --git a/modules/kdf-ctr-mode-node/test/kdfctr.test.ts b/modules/kdf-ctr-mode-node/test/kdfctr.test.ts index 10ab7a8e1..6a525d410 100644 --- a/modules/kdf-ctr-mode-node/test/kdfctr.test.ts +++ b/modules/kdf-ctr-mode-node/test/kdfctr.test.ts @@ -10,7 +10,7 @@ import { SupportedDigestAlgorithms, SupportedDerivedKeyLengths, } from '../src/kdfctr' -import { rawTestVectors, testVectors } from './testvectors' +import { rawTestVectors, testVectors } from './fixtures' import { createHash } from 'crypto' describe('KDF Ctr Mode', () => { @@ -43,52 +43,6 @@ describe('KDF Ctr Mode', () => { ).to.throw('The nonce must be provided') }) - it('Precondition: the ikm must be 32 bytes long', () => { - const invalidIkm = Buffer.alloc(31) - expect(() => - kdfCounterMode({ - digestAlgorithm, - ikm: invalidIkm, - nonce, - purpose, - expectedLength, - }) - ).to.throw(`Unsupported IKM length ${invalidIkm.length}`) - - expect(() => - kdfCounterMode({ - digestAlgorithm, - ikm: Buffer.alloc(32), - nonce, - purpose, - expectedLength, - }) - ).to.not.throw() - }) - - it('Precondition: the nonce must be 16 bytes long', () => { - const invalidNonce = Buffer.alloc(17) - expect(() => - kdfCounterMode({ - digestAlgorithm, - ikm, - nonce: invalidNonce, - purpose, - expectedLength, - }) - ).to.throw(`Unsupported nonce length ${invalidNonce.length}`) - - expect(() => - kdfCounterMode({ - digestAlgorithm, - ikm, - nonce: Buffer.alloc(16), - purpose, - expectedLength, - }) - ).to.not.throw() - }) - it('Precondition: the expected length must be 32 bytes', () => { const invalidExpectedLength = 31 as SupportedDerivedKeyLengths expect(() => @@ -296,15 +250,14 @@ describe('KDF Ctr Mode', () => { for (const testVector of testVectors) { const { name, hash, ikm, info, L, okm, purpose } = testVector it(name, () => { - expect( - kdfCounterMode({ - digestAlgorithm: hash, - ikm, - nonce: info, - purpose, - expectedLength: L, - }) - ).to.deep.equals(okm) + const test = kdfCounterMode({ + digestAlgorithm: hash, + ikm, + nonce: info, + purpose, + expectedLength: L, + }) + expect(test).to.deep.equals(okm) }) } }) diff --git a/modules/kdf-ctr-mode-node/test/testvectors.ts b/modules/kdf-ctr-mode-node/test/testvectors.ts deleted file mode 100644 index 950f25487..000000000 --- a/modules/kdf-ctr-mode-node/test/testvectors.ts +++ /dev/null @@ -1,218 +0,0 @@ -// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -// based on https://github.com/aws/aws-cryptographic-material-providers-library/blob/main/AwsCryptographyPrimitives/test/TestKDF_TestVectors.dfy - -import { - SupportedDigestAlgorithms, - SupportedDerivedKeyLengths, -} from '../src/kdfctr' - -interface InternalTestVector { - name: string - hash: SupportedDigestAlgorithms - ikm: Buffer - info: Buffer - L: number - okm: Buffer -} - -interface TestVector extends InternalTestVector { - purpose: Buffer - L: SupportedDerivedKeyLengths -} - -const PURPOSE = Buffer.from('aws-kms-hierarchy', 'utf-8') - -const b1: InternalTestVector = { - name: 'B.1 Test Case 1', - hash: 'sha256', - ikm: Buffer.from([ - 226, 4, 214, 212, 102, 170, 213, 7, 255, 175, 109, 109, 171, 10, 91, 38, 21, - 44, 158, 33, 231, 100, 55, 4, 100, 227, 96, 200, 251, 199, 101, 198, - ]), - info: Buffer.from([ - 123, 3, 185, 141, 159, 148, 184, 153, 229, 145, 243, 239, 38, 75, 113, 177, - 147, 251, 167, 4, 60, 126, 149, 60, 222, 35, 188, 83, 132, 188, 26, 98, 147, - 88, 1, 21, 250, 227, 73, 95, 216, 69, 218, 219, 208, 43, 214, 69, 92, 244, - 141, 15, 98, 179, 62, 98, 54, 74, 58, 128, - ]), - L: 32, - okm: Buffer.from([ - 119, 13, 250, 182, 166, 164, 164, 190, 224, 37, 127, 243, 53, 33, 63, 120, - 216, 40, 123, 79, 213, 55, 213, 193, 255, 250, 149, 105, 16, 231, 199, 121, - ]), -} - -const b2: InternalTestVector = { - name: 'B.2 Test Case 2', - hash: 'sha256', - ikm: Buffer.from([ - 174, 238, 202, 96, 246, 137, 164, 65, 177, 59, 12, 188, 212, 65, 216, 45, - 240, 207, 135, 218, 194, 54, 41, 13, 236, 232, 147, 29, 248, 215, 3, 23, - ]), - info: Buffer.from([ - 88, 142, 192, 65, 229, 115, 59, 112, 49, 33, 44, 85, 56, 239, 228, 246, 170, - 250, 76, 218, 139, 146, 93, 38, 31, 90, 38, 136, 240, 7, 179, 172, 36, 14, - 225, 41, 145, 231, 123, 140, 184, 83, 134, 120, 97, 89, 102, 22, 74, 129, - 135, 43, 209, 207, 203, 251, 57, 164, 244, 80, - ]), - L: 32, - okm: Buffer.from([ - 62, 129, 214, 17, 60, 238, 60, 82, 158, 206, 223, 248, 154, 105, 153, 206, - 37, 182, 24, 193, 94, 225, 209, 157, 69, 203, 55, 106, 28, 142, 35, 116, - ]), -} - -const b3: InternalTestVector = { - name: 'B.3 Test Case 3', - hash: 'sha256', - ikm: Buffer.from([ - 149, 200, 247, 110, 17, 54, 126, 181, 85, 38, 162, 179, 147, 174, 144, 101, - 131, 209, 203, 221, 71, 150, 33, 70, 245, 6, 204, 124, 172, 18, 244, 100, - ]), - info: Buffer.from([ - 202, 214, 14, 144, 75, 158, 156, 139, 254, 180, 168, 26, 127, 103, 211, 189, - 220, 192, 94, 100, 37, 88, 112, 64, 55, 112, 243, 83, 58, 230, 221, 99, 76, - 234, 165, 108, 83, 230, 136, 189, 19, 122, 230, 1, 137, 53, 243, 75, 159, - 176, 132, 234, 72, 228, 198, 136, 246, 187, 179, 136, - ]), - L: 32, - okm: Buffer.from([ - 202, 250, 92, 160, 63, 95, 190, 42, 36, 32, 4, 171, 203, 211, 222, 16, 89, - 199, 64, 123, 30, 229, 121, 37, 81, 36, 175, 24, 155, 224, 181, 86, - ]), -} - -const b4: InternalTestVector = { - name: 'B.4 Test Case 4', - hash: 'sha256', - ikm: Buffer.from([ - 77, 5, 57, 31, 214, 251, 30, 41, 46, 120, 171, 150, 25, 177, 183, 42, 125, - 99, 238, 89, 215, 67, 93, 215, 24, 151, 185, 255, 126, 231, 174, 112, - ]), - info: Buffer.from([ - 240, 120, 230, 249, 183, 248, 45, 100, 85, 79, 166, 182, 4, 200, 8, 241, - 155, 31, 106, 214, 114, 125, 183, 170, 111, 28, 134, 105, 78, 16, 75, 82, - 86, 200, 180, 3, 153, 25, 100, 100, 129, 215, 234, 36, 82, 199, 44, 23, 163, - 232, 215, 211, 145, 98, 133, 70, 10, 165, 235, 129, - ]), - L: 32, - okm: Buffer.from([ - 107, 22, 232, 245, 59, 131, 26, 165, 232, 107, 249, 122, 92, 79, 163, 125, - 8, 155, 193, 114, 218, 90, 30, 127, 102, 45, 212, 165, 149, 51, 154, 183, - ]), -} - -const b5: InternalTestVector = { - name: 'B.5 Test Case 5', - hash: 'sha256', - ikm: Buffer.from([ - 15, 104, 168, 47, 241, 103, 22, 52, 204, 145, 54, 197, 100, 169, 224, 42, - 118, 118, 33, 221, 116, 161, 191, 92, 36, 18, 155, 128, 130, 20, 183, 82, - ]), - info: Buffer.from([ - 100, 133, 153, 128, 156, 44, 78, 124, 106, 94, 108, 68, 159, 0, 49, 235, - 245, 92, 54, 97, 168, 149, 180, 77, 176, 87, 46, 232, 128, 131, 177, 244, - 177, 38, 2, 170, 85, 252, 29, 241, 80, 166, 90, 109, 110, 237, 160, 170, - 121, 164, 52, 161, 3, 155, 145, 181, 165, 143, 199, 241, - ]), - L: 32, - okm: Buffer.from([ - 226, 151, 100, 15, 119, 104, 72, 93, 74, 110, 124, 254, 36, 95, 139, 250, - 132, 112, 13, 153, 118, 38, 146, 234, 26, 66, 92, 204, 2, 117, 232, 245, - ]), -} - -const c1: TestVector = { - name: 'C.1 Test Case 1', - hash: 'sha256', - ikm: Buffer.from([ - 125, 201, 189, 252, 37, 52, 4, 124, 254, 99, 233, 235, 41, 123, 119, 82, 81, - 73, 237, 125, 74, 252, 233, 198, 68, 15, 53, 14, 97, 239, 62, 208, - ]), - info: Buffer.from([ - 119, 218, 233, 62, 104, 155, 88, 29, 62, 6, 235, 1, 200, 211, 186, 2, - ]), - purpose: PURPOSE, - L: 32, - okm: Buffer.from([ - 188, 232, 152, 114, 85, 137, 174, 192, 143, 152, 52, 179, 184, 15, 220, 63, - 241, 115, 144, 126, 85, 116, 231, 41, 253, 206, 18, 124, 247, 109, 183, 204, - ]), -} - -const c2: TestVector = { - name: 'C.2 Test Case 2', - hash: 'sha256', - ikm: Buffer.from([ - 80, 22, 113, 23, 118, 68, 10, 32, 75, 169, 199, 192, 255, 220, 214, 60, 182, - 1, 126, 147, 171, 233, 110, 177, 35, 145, 217, 129, 30, 9, 80, 159, - ]), - info: Buffer.from([ - 210, 241, 192, 158, 103, 66, 27, 35, 143, 66, 168, 189, 82, 171, 211, 252, - ]), - purpose: PURPOSE, - L: 32, - okm: Buffer.from([ - 54, 206, 174, 72, 237, 133, 85, 156, 93, 53, 120, 152, 118, 82, 89, 33, 114, - 98, 204, 236, 138, 57, 162, 118, 85, 92, 199, 232, 240, 252, 92, 97, - ]), -} - -const c3: TestVector = { - name: 'C.3 Test Case 3', - hash: 'sha256', - ikm: Buffer.from([ - 57, 90, 16, 46, 83, 54, 189, 241, 27, 242, 237, 236, 246, 66, 54, 226, 74, - 112, 79, 156, 208, 13, 148, 71, 117, 211, 139, 57, 73, 69, 122, 236, - ]), - info: Buffer.from([ - 51, 15, 183, 124, 82, 229, 249, 86, 117, 148, 237, 162, 27, 243, 173, 108, - ]), - purpose: PURPOSE, - L: 32, - okm: Buffer.from([ - 22, 55, 236, 141, 159, 163, 250, 236, 86, 47, 225, 103, 156, 225, 228, 146, - 166, 45, 244, 39, 136, 163, 205, 200, 116, 193, 20, 147, 112, 254, 210, 194, - ]), -} - -const c4: TestVector = { - name: 'C.4 Test Case 4', - hash: 'sha256', - ikm: Buffer.from([ - 152, 192, 25, 223, 239, 154, 175, 67, 237, 250, 184, 146, 228, 243, 227, 1, - 128, 247, 228, 152, 142, 131, 149, 41, 60, 70, 244, 58, 166, 234, 86, 189, - ]), - info: Buffer.from([ - 243, 160, 102, 127, 219, 137, 115, 38, 187, 216, 48, 80, 151, 168, 148, 71, - ]), - purpose: PURPOSE, - L: 32, - okm: Buffer.from([ - 191, 112, 86, 234, 220, 233, 122, 154, 100, 188, 230, 238, 239, 155, 54, 32, - 97, 35, 51, 160, 121, 235, 42, 64, 145, 105, 15, 153, 162, 89, 9, 156, - ]), -} - -const c5: TestVector = { - name: 'C.5 Test Case 5', - hash: 'sha256', - ikm: Buffer.from([ - 166, 236, 116, 51, 140, 189, 192, 175, 42, 154, 51, 26, 208, 149, 76, 159, - 174, 162, 207, 4, 108, 232, 196, 240, 12, 57, 228, 155, 97, 75, 42, 66, - ]), - info: Buffer.from([ - 236, 169, 233, 45, 43, 25, 122, 243, 152, 191, 154, 55, 45, 134, 159, 220, - ]), - purpose: PURPOSE, - L: 32, - okm: Buffer.from([ - 156, 11, 20, 251, 100, 227, 163, 161, 30, 45, 242, 2, 248, 246, 44, 11, 88, - 132, 189, 175, 95, 96, 61, 44, 98, 160, 212, 136, 140, 222, 57, 151, - ]), -} - -export const rawTestVectors = [b1, b2, b3, b4, b5] -export const testVectors = [c1, c2, c3, c4, c5] From cf464c6e4dd92f9a6dee3137f6ff380afa0794d2 Mon Sep 17 00:00:00 2001 From: seebees Date: Mon, 13 Jan 2025 15:55:15 -0800 Subject: [PATCH 17/25] Add H-Keyring vectors --- .../decrypt-node/test/compatibility.test.ts | 235 ++++++++++++++---- modules/decrypt-node/test/fixtures.ts | 125 +++++++++- .../kms_mrk_discovery_keyring_node.test.ts | 1 - 3 files changed, 311 insertions(+), 50 deletions(-) diff --git a/modules/decrypt-node/test/compatibility.test.ts b/modules/decrypt-node/test/compatibility.test.ts index 8108522b4..4d5336d38 100644 --- a/modules/decrypt-node/test/compatibility.test.ts +++ b/modules/decrypt-node/test/compatibility.test.ts @@ -12,53 +12,46 @@ import { NodeDecryptionMaterial, NodeEncryptionMaterial, } from '@aws-crypto/material-management-node' -import { buildDecrypt } from '../src/index' +import { buildDecrypt, DecryptOutput } from '../src/index' +import { buildEncrypt } from '@aws-crypto/encrypt-node' import * as fixtures from './fixtures' chai.use(chaiAsPromised) const { expect } = chai import { + AlgorithmSuiteIdentifier, CommitmentPolicy, MessageFormat, needs, + NodeBranchKeyMaterial, } from '@aws-crypto/material-management' -import { KmsKeyringNode } from '@aws-crypto/kms-keyring-node' +import { + KmsHierarchicalKeyRingNode, + KmsKeyringNode, +} from '@aws-crypto/kms-keyring-node' +import { BranchKeyStoreNode } from '@aws-crypto/branch-keystore-node' + +import { deserializeFactory } from '@aws-crypto/serialize' +import { NodeAlgorithmSuite } from '@aws-crypto/material-management-node' +import { readFileSync, writeFileSync } from 'fs' +const toUtf8 = (input: Uint8Array) => + Buffer.from(input.buffer, input.byteOffset, input.byteLength).toString('utf8') +const deserialize = deserializeFactory(toUtf8, NodeAlgorithmSuite) const { decrypt } = buildDecrypt(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) +const { encrypt } = buildEncrypt(CommitmentPolicy.REQUIRE_ENCRYPT_ALLOW_DECRYPT) describe('committing algorithm test', () => { fixtures.compatibilityVectors().tests.forEach((test) => { it(test.comment, async () => { - const { - ciphertext, - status, - 'plaintext-frames': plaintextFrames, - commitment, - 'message-id': messageId, - 'encryption-context': encryptionContext, - } = test + const { ciphertext, status } = test const keyring = buildKeyring(test) if (status) { - const { plaintext, messageHeader } = await decrypt( - keyring, - ciphertext, - { - encoding: 'base64', - } - ) - needs( - plaintextFrames && messageHeader.version === MessageFormat.V2, - 'Message Failure' - ) - - expect(plaintext.toString()).to.equal(plaintextFrames.join('')) - expect( - Buffer.from(messageHeader.suiteData).toString('base64') - ).to.deep.equal(commitment) - expect( - Buffer.from(messageHeader.messageId).toString('base64') - ).to.deep.equal(messageId) - expect(messageHeader.encryptionContext).to.deep.equal(encryptionContext) + const output = await decrypt(keyring, ciphertext, { + encoding: 'base64', + }) + + ExpectCompatibilityVector(test, output) } else { await expect( decrypt(keyring, ciphertext, { encoding: 'base64' }) @@ -67,26 +60,172 @@ describe('committing algorithm test', () => { }) }) + fixtures.hierarchicalKeyringCompatibilityVectors().tests.forEach((test) => { + it(`Decrypt test: ${test.comment}`, async () => { + const { ciphertext, status } = test + const keyring = buildKeyring(test) + needs(status, 'Unexpected Status') + const output = await decrypt(keyring, ciphertext, { + encoding: 'base64', + }) + + ExpectCompatibilityVector(test, output) + }) + }) + + fixtures.hierarchicalKeyringCompatibilityVectors().tests.forEach((test) => { + let once = false + it(`Encrypt test: ${test.comment}`, async () => { + const { plaintextBase64, status } = test + const keyring = buildKeyring(test) + needs(status, 'Unexpected Status') + needs(plaintextBase64, 'Nothing to encrypt') + + const suiteId = AlgorithmSuiteIdentifier.ALG_AES256_GCM_IV12_TAG16_HKDF_SHA512_COMMIT_KEY + once = true + + const encryptOutput = await encrypt(keyring, plaintextBase64, { + encoding: 'base64', + suiteId, + }) + + const decryptOutput = await decrypt(keyring, encryptOutput.result) + expect(decryptOutput.plaintext.toString('base64')).to.equal( + plaintextBase64 + ) + }) + }) + + function ExpectCompatibilityVector( + { + 'plaintext-frames': plaintextFrames, + plaintextBase64, + commitment, + 'message-id': messageId, + 'encryption-context': encryptionContext, + }: fixtures.VectorTest, + { plaintext, messageHeader }: DecryptOutput + ) { + needs(messageHeader.version === MessageFormat.V2, 'Message Failure') + + if (plaintextBase64) { + expect(plaintext.toString('base64')).to.equal(plaintextBase64) + } + if (plaintextFrames) { + expect(plaintext.toString()).to.equal(plaintextFrames.join('')) + } + expect( + Buffer.from(messageHeader.suiteData).toString('base64') + ).to.deep.equal(commitment) + expect( + Buffer.from(messageHeader.messageId).toString('base64') + ).to.deep.equal(messageId) + expect(messageHeader.encryptionContext).to.deep.equal(encryptionContext) + } + function buildKeyring(test: fixtures.VectorTest) { - if (test['keyring-type'] === 'aws-kms') { - return new KmsKeyringNode({ discovery: true }) + switch (test['keyring-type']) { + case 'aws-kms': + return new KmsKeyringNode({ discovery: true }) + case 'static': + const dataKey = Buffer.alloc(32, test['decrypted-dek'], 'base64') + + return new (class TestKeyring extends KeyringNode { + async _onEncrypt(): Promise { + throw new Error('I should never see this error') + } + async _onDecrypt(material: NodeDecryptionMaterial) { + const unencryptedDataKey = dataKey + const trace = { + keyNamespace: 'k', + keyName: 'k', + flags: KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY, + } + return material.setUnencryptedDataKey(unencryptedDataKey, trace) + } + })() + + case 'static-branch-key': + // This is serious hackery. + // This is *NOT* recommended. + // The proper extension point for the KeyStore is _only_ the Storage interface! + // However, this does let us do some quick test vector testing. + // At this time this is overly perscriptive, + // but the expectation is to be able to depracate this + // in favor of the test vectors project (integration-node) + const keyStore = { + __proto__: BranchKeyStoreNode.prototype, + kmsConfiguration: { + getRegion() { + return null + }, + }, + + getKeyStoreInfo() { + return { + logicalKeyStoreName: 'logicalKeyStoreName', + } + }, + + async getBranchKeyVersion( + branchKeyId: string, + branchKeyVersion: string + ): Promise { + needs( + branchKeyId == 'bd3842ff-3076-4092-9918-4395730050b8', + branchKeyId + ) + needs( + branchKeyVersion == 'e9ce18a3-edb5-4272-9f86-1cacb7997ff6', + branchKeyVersion + ) + + return new NodeBranchKeyMaterial( + Buffer.from( + 'tJwf65epYvUt5HMiQsl/6jlvLxS0tgdjIuvFy2BLIwg=', + 'base64' + ), + branchKeyId, + branchKeyVersion, + {} + ) + }, + async getActiveBranchKey( + branchKeyId: string + ): Promise { + needs( + branchKeyId == 'bd3842ff-3076-4092-9918-4395730050b8', + branchKeyId + ) + + return new NodeBranchKeyMaterial( + Buffer.from( + 'tJwf65epYvUt5HMiQsl/6jlvLxS0tgdjIuvFy2BLIwg=', + 'base64' + ), + branchKeyId, + 'e9ce18a3-edb5-4272-9f86-1cacb7997ff6', + {} + ) + }, + + storage: { + _config: {}, + getKeyStorageInfo() { + return { + logicalName: 'logicalKeyStoreName', + } + }, + }, + } as any + + return new KmsHierarchicalKeyRingNode({ + branchKeyId: 'bd3842ff-3076-4092-9918-4395730050b8', + keyStore, + cacheLimitTtl: 1, + }) } - needs(test['keyring-type'] === 'static', 'wtf yo') - const dataKey = Buffer.alloc(32, test['decrypted-dek'], 'base64') - return new (class TestKeyring extends KeyringNode { - async _onEncrypt(): Promise { - throw new Error('I should never see this error') - } - async _onDecrypt(material: NodeDecryptionMaterial) { - const unencryptedDataKey = dataKey - const trace = { - keyNamespace: 'k', - keyName: 'k', - flags: KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY, - } - return material.setUnencryptedDataKey(unencryptedDataKey, trace) - } - })() + needs(false, 'Unexpected keyring-type:' + test['keyring-type']) } }) diff --git a/modules/decrypt-node/test/fixtures.ts b/modules/decrypt-node/test/fixtures.ts index ae8f96c77..9b5712834 100644 --- a/modules/decrypt-node/test/fixtures.ts +++ b/modules/decrypt-node/test/fixtures.ts @@ -104,11 +104,12 @@ export interface VectorTest { header: string status: boolean 'decrypted-dek': string - 'keyring-type': 'static' | 'aws-kms' + 'keyring-type': 'static' | 'aws-kms' | 'static-branch-key' 'plaintext-frames'?: string[] exception: null | string comment: string frames?: string[] + plaintextBase64?: string footer?: string } export function compatibilityVectors(): { @@ -1238,3 +1239,125 @@ export function compatibilityVectors(): { ], } } + +export function hierarchicalKeyringCompatibilityVectors(): { + tests: VectorTest[] + title: string + date: string + status: string +} { + return { + title: 'AWS Encryption SDK - Hierarcical Keyring compatablity vectors', + date: '2025-01-09', + status: '1.0 Release of the keyring', + + // + // file://ciphertexts/28eb4632-5fad-4324-9fa8-55c55dac61fe + // file://ciphertexts/d88cfadc-4595-49a4-b38f-c3d01102c4ed + // Create Two From Here + // Create Two From Here + tests: [ + { + ciphertext: + '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', + commitment: 'bP7qbk9OboovlpWldxTAktskqzynAmrHqO6xF4bPrrk=', + 'content-encryption-key': 'Not tested', + 'decrypted-dek': 'Not tested', + exception: null, + header: + 'AgR4TfvRMU2dVZJbgXIyxeNtbj5eIw8BiTDiwsHyQ/Z9wXkAAAABAAxQcm92aWRlck5hbWUAGUtleUlkAAAAgAAAAAz45sc3cDvJZ7D4P3sAMKE7d/w8ziQt2C0qHsy1Qu2E2q92eIGE/kLnF/Y003HKvTxx7xv2Zv83YuOdwHML5QIAABAAF88I9zPbUQSfOlzLXv+uIY2+m/E6j2PMsbgeHVH/L0wLqQlY+5CL0z3xnNOMIZae', + 'encryption-context': {}, + 'keyring-type': 'static-branch-key', + 'message-id': 'zvIUN/0RXufuz61EC6mqTygivSIVh9gFx8IlrULGa2Q=', + plaintextBase64: + 'epKK+3xUqSh45m+YPhayfC7v3rvjtg/datanYiXUOzPUFoseOMI/6bdLKFDTbNIwo/N/pcTA2KMO/xFtRztuFBCVxvX40yc5L1BSDdi/L2IsuEBLEl3R/WK0/uotAVXn5ObliPKyjNO0TTYEL0Oa07IQN0EDqnc61T/vMEpAJbb9i0tnCWkKUlw/1z5cxeF0Vixs2cF0OEQqv9THQkJb4b/SX9EI04CeU+C/eSVfMJjEbShDK0vmLn0jwObZVzYkOXBYMi8pj29jgqfZ1fJm6njWAiBVYlz6IEtF1+0j85TCACHRD480MDsFGDPjJykj5v7NxDBhBonB6bvVvb91eFh2It8hT6HheOprPP4PVs+0C/AzZGgl0aQ+wq9Ll/QILU+zfm9FX4cN2XOQlUFA1vGoC6G/5BiWRZTa2MhOUlvLpvCOPy5DoQ+TmyYDHraOZgjLZZURwmkYmqCttbsEbzmQ2KU1V5dg6JmAyKTRpITkv0oa1UvEINUsibZ+5qTMUNB+Ofh8xeO0wQofX8fnJ4SJKg4F3AxmWVHTcMjPTR+R2XfSCbFmOHPKMBaBznuUeCrY0tMFIPfa8X5MB+lorgWGofMGMyI+1CF+EdeuaqbDJhg/GtB7IJTzG4d1BxAJu1c+iBpcR4QKkUYtEpaP10ggcpb1h4RT/4eiL7GB+GWqgfO9vmVIhHBb0MXE9mOx0ihtwlJ7QwSvrrE8O6Aqh8I6v1UTBcDSNcC2fF/hP68ECfTeyBtQK+dDhRff0aHRuJ6AiPfM1SjEVVazabuCh9Q5IvirNIh2kg0LaB7IQ3H8wtYQyF0zLeyyLpwRUG9jYKNR0PVxZOa4Wc5SPbvRbnPRR30JExQi5p06WvlJAkHdUt5Y0HO3Xsht5y3JHgzoqTACU3WQsBdFG7gn5UhS90q4mxTbmeNMDpWA5uKVY7YoqqPCzJTSTj5hDpkPumIIL1wuJgIZ6RLIOSX6uoDrcOeMvYHFaLWLgS9CIVM3UAM95FqRp+b/QFs3LrmVHH9KnX7qKx649WPgRd/2nF4TpBOk2QvE8Hr41csh6gqy+eSSq+WL63+9KAKwFvR9sa81wKGjwZjiRLnjlow6Jo8TYSw6ms9FXTNzMDnpvIWQgZKQ8t3/7VP2r33XeOqg6/ZGboC4l5e1eoPe4FG0eXuG2tOUE/vTguLzVh5RK0sn9Z/evTigCO7XQmp1QMhsqe1xf0KAKpF0GHBycTqvPhN2YUmz+WYbgPFK9GNaJkcR20KAHFfV0HnxlAXANUVTlJRzjiJBseyDBXVhvcMt+AsuIz7vlCQfHrKvZGnb8I1uk98svFThtyejT36jgWbTUjNbRz0gL8IK/ZzxiW847MQTrwq6azODtXXSVRSasMiwQwzg8SXUFk+4F32XHcnXv1UTYsroQvPC5tiwOU5+jvT/X9BIsgkHoiWqCykCiS0m/3uacA1b4w0QT1Ixu7qEs370dL5nhCbSXJKVEEhVGokGxUM7CGJeXzpOq/VL40RvpYbz03CrZJQGE9vTFIlXLNkUBqG8LLMVIqxzkXgcGzbL6XvUV+FFMb80SU7B8tpIxn/9mmQyTcpAQZV9oSgPZ/0zH97BYBgQIV4ZLCiI9g9ppKFhoNIqO9Gs8pwJn2NTq3EdwKtiZiC7GlDC3UXDiTu2sJ3Rfyj1plTleDIgXbwSuv8ROMFFEvmjoDd80PjGrOntpzobEjTHfHomHr8ZZu177wgPS8oNeFXT36T2K1IY1aX/rFj0nShN2rUd3v9UM7Kzu5CnqCsdNa93jL35B6Cgt0aVgmAho+KepI0auQd2Mhahnd/2n8feemy9LlFd/+2PiqZA2NAqx4Jk5yVE3JFF5jDmvtEJ045cvkl6+nxuRYqRgVQ1DnyqqUc2QeeqaPXrNP8vkFYC9++KdsTOaisbxuAP1LtWwfiGfP/l0UayMt+h+kecpxTI5rLL4d3UuNV9eXAjn540KiyGrKanSdZiLuOxIvokLJt0Ct7WL/7pFW3qMWVILLHQIEQFl82IC4Jjc5LKtcvcBLdYf9/0GVBhhvtW86kk3AUNHoMrni058YvzJSajHqSyZRKvLvIoZ8LiMBzCo+oBNLMpKljCHs4sksgsc+dEGlKb06vbaZMJdmvVywkyeFMaMR//I5msuo0qX4+MQRT6H9mFoPJQHLa3jjgM8xIhGjjH4HsyfBo1agN4C0ANNPzOgMmjFbYGOwg56UTtR63u9AZOQFNhKeEbHXjv0kAgWYxaK+1lJ3l26vpv9jcpAghIcl9yxYfFMNdJPMaFpmXrKQX5OXQaqVcUIv/SjIDaKOBBL7z0mCZFTbLjvWSVpEYkmmLcVOatV7UuRz+i4BHjEQ7wEtQvZQnKrsub7N/Vfy3sACn8rVy1nwvd3Ij1TXaZpn8ohVFIndy6Aps902LcRe0E7jvLUyOzPAaByOARLlHz19klB7rr8lS3XvMvqFKnSsdGFzEZq0r/VnC5QknhGtYT1Ac5qvVhgOvu9h0nYvwGSyrE88sUEOqAfwufRcjybFF123G6IpjzYEXw59dufcTT3gbHNG+pZE0tOPeaITFH40CrkZe2lHkcocoGySfnqrBbJmY9/ZzGBheAtJQEwq6E52g35sl2aXBxmtAk0LigKwh0iF1ABl/PwcRHiI1JOxH3FxfeecC4xbNU26b7RwEAb6RHHNo/50x62g68/vG1GlPULek2WVrXl/kx4+3fV2cTNtubLeeP4/x3DsfEQSUr7fn/SBRUg3JmuPqZc09HaXPJiAi0RekGEA+EwRh2nD4qes8D/aLdUPGlW6neLjqZ5PNCT8XCxG2C3iF5yv6/IfskjI17EuACeIVsiousAsLiyGL1w3rAHE1nGy1hA5+uj7v+xu+3ayOv/WmN+6V5QOiaYGoicUjeKLP+DHd4Z0WS7IPkUHVGvEkdK/gHGV2GuMk6tk/Z6hT/GlWOIrHf3RTE6fJ+CtNPE4Ruv4OGKVwVbephdMoPvoGn/Qqh/9Tawl/jVdzdbv2dd1WVMHlD0kM1SmE8T50qQrXV/a+B4FCKuDHlVLAygZAd2jrbUsVxCHfvAP9LQVdDxsfG0Vz3Q71FQItwa4poqfFzcehp/BfCEKZUFN1Ppc3gc/Da2EY8O0GL/XlZkfuZ++cM2gqLtxJ1e10mnQoYAE8lNYgWI5ChVKm4QgEZzAT4acY2NPr2fRghI66Ldm+8i2j0fEILLWM4tMISyc0lxCfu1EJIzdkqCNjSZxULP6bPgeUDsIisPyaMFSIRiSvtwMVAtxXcb0NaYuV8i4MK1StOQP9c8n3YJdIZa4SOSWIMffGm7FMLg3pvpnRjTqcSM/WXSkv/z0Iecw4lEnT8b8xHVs6HQl0QSfkJK5S5wPD3Z345PbAA6KYT4xk53WqbTSaAibxEshEBqhoDKjvDlWxckt+zoKMTYaD6sdqnd/HVcsnhmLRIrUJnpPadOhjn84UcG7TOEYxyebnK0YDGDkcVXKYcHQ7hTGgAbUgwT4DpEB4wD+8lzeGumWTderXxwDB8hd5giZ8XfhzJq3sHMYXk0wdJqfwm7zR1UZBS6MpX0YEb4QFJheMI009803rHBt3gWFQuZYaXaUWHc201rixI1aOdlrKVT2VnPkVOmen9oKAQrnjo9Y09bnPaHqsulnpjKjMzDGxGNKDw97OkQm1ofFdvOWFcON4WefdT4/UAlTrgbC06pBmaf/+7a8diNTG7am95ojnyKZyrxALuoxK3FfMKgTKPyt8PQF4LoZPUYYcsVYrJ3KAlz/JxrXKjnG9RlqabCq3AIXc0+gt+SLQT5zLMuCvaZc/0mil+F8zMd/zTOASRQ7LoIDD+hUYolhFEcAnADBq1uhdsm+UiiAb2Rx3Q95fNM5VaZs56vx6ict/0bJnlzuWhaeQgOMMR4Uq9fKvdoeXv9OEA5T/3FEb2lG15B7Tm2ATRpVTFgUqlU/mlc3Fhjpfws7YGBs+1T+T/Hve+y+FSadD7COgX85zVienC3kwymooaValmhh6SZXb6kEouX8IgGroGzi399SRVpAablqGUDcOgyBR3OD2iszYcmqhU4t8iCfaGSgEDenw1HEB4k0JIV8sdSfQk4ttroHnG/8YTbgVNduQlqJFwYLge4SI6URY5KPueu2HuGyk4HKksCWH4JTuNIh6dCbkYSZhf8bBF5Be++Sm682u4sLZnbSLKqQqqIDzcvmw94RufL2D3I6/0hyhAqD1lbmCuFes0XIh14HL0Z3IQQy5XphfRLo9oy1VQ5/HHH7DDOYNabAy923hxrTHwPdHcsa2llEvb/7/06DoQn1fjTEHCS6sHGDiI495YToYN4qEYmjdEH5szUnpqtWxC6dQo8cWQzkLuGUUHOENUH2e75iun/zikEvNKRsN4fASwAEPxxQa4RTg/5HjJBChXqOVDAgNRzcL11hta6dbDsQU7wvHQvZr2k6JxK2Pewy9dHv/ySGVIX3moQTiK/H4sDQd/8WXlQfc2Yz1v5EHk+1Ky9hjhNrV+hKvcSUxkDH7IVz6Vj+majsIuImqF6ShgNWIuEEhRbMvqbZRmmfQ80SgAx5XmnOXwcAffGV03kaWXTpeCGIyniTj/Ur8n9LrvgzWHd94H1ckcLgcm6RcP2xj3taTsqqj2Wb1+Gu4X4/Ls/5ihiM7E+Zx1IPm9iFNYgiKd5CWfSYlceS5yJ3VHqjBueVfwJv/GmWaE1+/2ZjbWlXG9AS2kw4QQT73trdceauv4O9KHi7ygsUW4Y5sR+tz6XYcpsXisjsdG/74fynx5G8nrZ2Nd4zDo2Or/0rrKfPY5dJA56DogxXfSdqWlQWEaX47yP/NGszIrLfHOWXmRvcZ1Xrqi+h3UjdmaWprm4FwgpYrGkBIxZFlBkBl6CdJTMYPN+hO/8l2VCej1M+JgCsoE0hx0m5Qbe0Nrf5DpTM37nx9lHzVE2cfmYyuYC2nCrfmljo714Ag4YRa4xy5dn71PlJw7JlZIxBWxFlaH9AJsB9kAvjf6I2CBTrRhSeei09wwEijMv3uvW/WvF85tnRXWvn4x7zxX051JIOK4Dq2ED4Lu6bEqtzLrHXNkFSHbVuoq+LHIFZfv+PVwSzUkCOI6Fa0wpAZBVM0Fynyw5xoDUd7QvHzyaYn9cgL1gO3w6fxbts4S0NBQcC1nXoSh0ZuS9qWI/NT896EAffde4WKluhJRwBzJTxDikqntB+Pxv9UstAib/n7kSO9fSQop6fluCNSPOkkuNCW1nJp0jeWuu1rXGwWVgli8gcVulN3pjlQFPoMSJLWs44J6EhaWJu1TwQoFbVlmRveq+CeheInpynOQodrTaJqlWbkCrEoGkZLuElQzVvBeaTtDGX7g5YkZaT8OLM7EGfOEe5DVUbxJUg04EjwZZyciCMbb60KzJjJkabEOp2WlT9vBQ0YSFNEjsignBmgcnyXN/3Kj2AqnKw7P3UlWTlkJj7MMhw0DSXo+ByjluUr6//n9ipWQcj47743qG1g6+Cu+3eGjaW/qo7ACwDyNcOycNVcIk0TcL7l3YNxsDoiavgXXjGT+s6cuVnng1WY7k65ndm77+mBBxPEthKR0vf9ohl76CLvLjVRs/0CgJF81Jrq0RtIAK2rHB+399V2rEN/YlPwM1UH8nr6ORa4ILBedggZZDp9iyeGqmUTHO+1nn+6bYEIfectfcMp4ZROy+Nob9R0up3Ae2TWkS798SBJ28auX1IlQEqRHSPaM6xsrOYqZw8kvFrKhg8L9gtcOlDuZ5zeT4sw7SAT1OQzhPNuctFyd1/D4ksOzYhZtnxu1GtSzPj+4Ou6ImhiScU9YCBZCvdMz5lGCSAm6JEAKJT0BVOV7jVSkVuXCyxaV0e/tcEUQRCBjIj5JDCWthPhGIlmG26qwRy/YB8/BMWmLVDuFCQ7cNZNZCY2EUoO4oFVbaQsE8APb00C3U+L7MCGj70JXvppkF8ZGN4HaYR/sYGxXx27wbm6rsVMhlU1TglaqNTZ+mgcneMmRUqUjBO52EfALXwYYRDU8KvnALmiHF/oFwfqNvFA3vkoM2EhE49KYgIX3cmDxvKRT0xqDlkOg+jD2SvWHHJbKec5Lc22KTV7COUXHLLT10MT7HXxCYMPE04UGeDbPXtD4SaiW8m3FFZE0KqaJuhaYSTanhQW5FLigIm+Qonjl0gJ0yAwpZxkIp3dcKCYfwWa51uQnsIWcZ+g/hsDWwcIWCdUpUXgFWUqh3m4I/67z9NtTauOX/S8pRadF1uu7oSWQ/QuMF0RZV1dwjPkjZiUxAFXRitU+lqPSVZ9/zFflQO1BWwZh0uJ3tTweErDrNYZS9xHKLIQK4GvShMjouPOw+TtOMW3sFCrFJKxgKU03TMP3s3ZuDqMsenD8gWa0XWTHH0NoMOw29qgAfrnFBvYnAKJ65axMmIrkzsp5nbIwCKbSstOn9d3zjQZohcGGsJorgaXu/4ArVH59yyQDMjCsKYpz8mUPOJP1hlh8gvL+4qf9NwSz2+WvyImrLM+laIsOS/OyThsrCVXQ7X+rEa+JwgtE2/URKeuQNfQGe8VCm6x13OEOABGag/vF2S65MvHd8Ewz4OIPXK5+tFFZpoy9tvoTzuh0sa0lnX/h/6j+Ni6ek7BGDnL57dMW++CVgpw9Wy0Vw+8cMre0wb7RwMrBWzlVEyRC+e0PFOtMO27tgrgYqmE60EvsPWx3beI8BFv1tvqwjo1jI8ZguOlo4F91oqZOkiwDw5E3sSLoAJawt+1HUuKYS5s85PLvaqlvt1zP156G+oo+Az5/87j6C4JmWBlmCaTpWHWwRoad/usrxu/bxmHWOldzPrlSGPogxXdMal0EjzjMIiqmFYcXn67GPAtDYAUtaI4fo+FmLI0vwpc5q1+HqkxrmQgqQDErwqAvKv3jITBgU1J50oY4D81+d5vy+D9FB5hhvR8CwxwujQszLfy+I8lNHR3/UKterJSp7aS2G4VKkdUPFhkzKg28A57InS+1b9bAPDxI7C0kzCiriKwxJqLtb31tHc/GdgtBvyZPdAhIop0kSeEFZMlp6khOx6CgKlBGvuL2wmrIbwkKiclkiVig+Z58BvHgdp+EiuaTMDTkxxx9yYzmva57IzJTqZ/FPo9PsllqPFtX4CbZp5aRbY0TbxXZA4I5My+8dsYSfW3AvVIwfDIDlR7wD6VCEYp/R3mwlkZ2U0K3W2j4hdeNSBlEB6joO5ctqXxrIGkAsJ19VvwV0XtB6M15R4U/nkF/sFO3q/cf38yN9rxyB0n39tZNyK8QINGDQW0V4OWXg/th3SBGUonNHH7s8LQatKBYIkeVTbxbg8zsUH009R/KQfqD7n3wj16UP9f3a1qgQd6f7w7WquUOMReb4uf1Ran+/uP8zUSDrbkvOwE24mE/k48dFM5S+jzcVtvFdMiBFSIG6QHX/lrzYQ1w461wxPlX+0i0zyI+ZV9v6YhRLQvwNS/xwEo9nyzamPkt7ozv+T/u/vSbQCruuuZ8zcumWRG4y1IVEkw5iaiIRdyDwksHQp6gv1N353T8rmtXU6idUPEM4528I3OhVb9ZIZPKkqreXZFdy+R369fVc/Tu5bi+P560i+YN3NiO/UdsRXJXNQRgMxZPr+5Q5prwrg3ha9bXlWGU5VUNDT+ug89Ihteu+S0G9+wmdBN8qg5JUPnHm1Bk6QEZTcmMU2c+yc9Zi0k/P42Jc4sHVVxzzWDkS9oO68gVOLSO4TSsJBvnVRHjh+RJ7Ftzu3r2D+0J3jsOZ3UZkIP49K4qiPClriiwOk1J2x22pDbI0NXzOX1NsY13rPwU+neEEEKluHvMKSgVSK1pM5Y927LkoQ9yIlJaPaXN9xVIBz31Z2eTX2yClYECqp4hIQZ3EeTpxhiDZS96/sqJBnPOWuHsIjEoXpQ1J+4GNYuzdPQxegXwrAysvWdpzl86uZLQSw5wxfEbF1jJqsHlukAO0lHLvv7jfO0ZTy/LErZAumcnz1wCb+AkXBaUq6waDHWt7VXQxsxXQK+yi8tIfneCQRH5jE3ZFhEH/Ks/wW94TynOREy4T1xrsebofC3PgmwQ2YTLvcDZzlq+uyqe6FjvjDypbDxoMgX/zvrOlbBkLPaYu7rCNsI80lA1c6NNW8pVJlWK2+bhmu4uReEYZBXqOCYlyqmjlIdOvoGFwef807viFYHnwe4xvbLbnhTtXAJutPsERCYppXElB+UrZ+iLBFdJ42x2+to1cZ4kFhdLYrm419SekW1Eu0y9iUVo/05OvYB9kC09eN0D5TzROt1IBrQVnwpvVhVZnLILYY4rkenA0Ruq6OReakSYBsBqaVMcmEAM4N65g+2nNNIO4tyGywDAAh3gCCIgAhBk1HFiqVJXPjA2mVluwAmyxn/mE35mdrGhG2HnlR3OrZ6TtjOdjeOjRMhO2FYuQrNNnv40Kdyaew3TLoVOS+gKLp7y587K1x6PAbhQgJesu7rHwiHlSnunQNKN3NyuvWLvSVfJ4NN2kJl/zM53PC6C1khkvqXLlk+jknoGffz/oMvecz1A65JD5PJGb4/x/uyDZ7MG7pAv2JIIoIRfZbAcxI5g5jAjWk+Edd7+Zk7RHYBc88RItTrNbqtTvxYcwTsAv6IN6dCpKVKipHPcutbVCt+CNy2rHVQuZ+henQURhh4IYAydxm5AsGtMRa+OQruUlRJ1dBQz8GdgiNOUkduwbLrJtaKUSDjvxiTc1OCRNZEvzFvBD/JJ07dtEJ1dzA3JJboGlyQmhtVRjPc1xeVt1yFSHP7ePjrfzljvqx4nDBSDbIdg139J6RY40i6Ii58PqruR0OTxCWLP255RMuqEi/+Tbzk8y+htkc2dhQSlyj+HHmn/d80/arHP4OD9nvCP4yeS0eiKmSOa83yASqGFeGFVaFNt9LIfEoVCWVnOhXVDaRvlj64eLFCPqlg99SxyX+PVCxBnotrK5Lxix3vhgS8yJsmMsArGuz/f+VVtr1EsL/btq4fNIFpfKFXeFSaHjFCSedXHha/WuQtAsKMcCBtX4cZBgu00izmqYy51OseTeVLbYqIvO5C13W88Yl/wFA51aeyjPbXir2ktr9TvAh8WzrCp9xVFe9k2DtL2E1Hv78U0wua5GwPOIslvFYqHhKAg9yncv57ywBsDcbyuo4GCAZuvCY9xvsk2Uxfd7o247J1X9eVPsowL1J/Jfo5/jT1FMw2tkuP3UWSp9nEYSxA3PG1nd2Xsn2VxmFTWCDLdn0WyoyvUaT1PU8hNZBD9vrelxWboyT42mqG+YCkYPdiq+vtucYnz9qL4RaZjPJ52PgqTa2b+6SJz1lFzingLixl1l6PNfg35g4YyrhypCkCtB1GV0uKZOguqlKtBLUYgZu2dDdVJxyXUSZMLyjO197EN3aT2ONMfOJMABy110H5rvrGZo1cv9Sv3J3idA4+u9Q1UmtpslzeAEsJLdSiI10k8EVDkrx3UIfIFwNtP4GjYYZSUCEmmK2qV1Au75CA1I98/o71yTP9kJRJJ4d/aIQO4dQ5MDUYe4MpzeH+AIF4CvluHxTQ/r+PcDZ2cI48dI+7PCtcMKGGcpMZgLkjIjiVCa8nLF3DhWy4nqafXkaLzIoHLOVTmSt4dn1jCrTS8Crp06UxLw2qxnlWnnz5baLUYZH9EicWt0arGB2K5d7zYlCJNYGvhVWFOMb/TUQ2fZsbfuoUjQCXgnm0ilzx4WdN/aywZ3JIYxF7YnUwlP63VkEGBJuyfNyCBy9WOmY+eCY6/reGtYyp43PzddJYcArx7r5E2Mlfvmn+shKm4gx7ae9Da0miedL3P2PMA8k15QUBDNwqdkcRIRI3oNqNmtAsK+2C8y+kq2VXQOBr2jSoSzZvTX7RjoRdQQ7hBH0ZL4/+AB45w3pnFeC34zZSOkD8KZjke0B7tn3arqamzq6EohPfLgKyleQMenK66HVBAoOHk6/2Gjq1FAY5MauNbUCiywK/9IXfPrKJeOxECUKrYXv3CTlyBwoF/oENxtVg/jUcWRjpnAu5+PXJ1LIbm1QOXiHE3uRYeTyqgbz8B/vYWtfpeZ8cxhi6N1prZSZg786VEWDNy+WydztCpSrWPWeDVkxAcg74s2QkJiYP7GOIltPl8VA/zUtmmCkI20defhTLmLApvqgewIdoKa5LKuNinStCAAlw3a9y/IWE/Qf7BX45DiKI4HaaVh5zYd5OIbComq9GKbLV4bTdWA1blucAcxYv5NeGbL8kdHvSnUZqU0EwnQUlP6vGnTCs14ZaR20DZD6TDyuvYcpJsSuGmey3SAfw6XhA69Py+UT+UizATeaAkBPVjlBsy3FKXU8OcMmQAsyGVByK19K0Qky8D70+RgFueG1xoDCjLK2RQ7npaaw5K29w2IH4zSgK+Bcvy8j9dS24D9RkQ75rkVzUU/6STL8erjae1d8vMM9sD0NE0RIxqQ/SGxaaxchOrjAr0OixqYyTE87lzFBJ+M8ypdZUqT06g96Oc500kq6odwmMnlk4k5Jjn2zgKFc0+5Vc5278anwz5hV8ehBCRDsZlhBqdFw43PuGw15WKnnEf++XVmbU7GX6bhmE0NrFMnMl6/CLAo4+GY5yCeGUsCk6akRWPGVc0W2OjYiysm5tXvc3Z2YyVr3lKiCvRgiSupK7mQrZinoYas2sDS0DmfOS+yv0GiSwUzOCjL9N3OQ1xg6bKBqQiBWfXOQyfG4EGDs0kZnTQ0GJ164um30CKt4/Bdf95L7ix+t3TozJNvQCuVry4AnztJ+eBzpcs+UIGqD0Gz9ObaaCTH6AeraVOnkVcMTC+oMaD5mtGXNyLNegRs1Eiqtbsk7kgivRNHcnKBELKBrmehVBQrojK3m/hORGkhxA+9oFIhjwy17EPCyhF+SpAXWni1jjmGVkL+TEHSi6WpNMOFAl+9XjUHcqkxdntvJNGvuzdMp4oK05t8n9eju/TSkuSY1gZyLxdA7Bai3Cu4WbZTrOmZO/1TzSXULfD3e/qv046+zR/aJeqRQdDBi9sbbUlk7/SWlme2wZdZzDHZ+CQXdrR/xWExlmE8BBKzH3SsB8innbawbzARDlWfwDA7qCw4hbjvk8yhjsaTQLwavLKgGnMbyzBj2qF0OiUx2mFVvIjjIHaDqTHAEb81/z7Hxbsc9Vzom/Qd5UXzPpKapb09BeJrgd0q6PbY9V1Fzx1cXsZ+qMPg6RBL9sz5mA0vUhuIINWSD+WsQUWqDaVt29bk4vvd2Wet/Kjh2M7TVJYZKRop1BJPKKHPypxb0PMsFPUIRGKw2ESwC+8uuutdL+DKRbLetEYOJWXVtfON1c6aglMC1bkKyOJ5osHshSy7aV6+W3JGn9fkl30mJqk2dWNS97lIbx8VOJuOOCpvxrV9GpOzjSlfgjpEaEkOpq04pTfjRuXQ0hmVpY1jfEewnHqG/JRmY5VAO2rYXzgAbLW/asIk8aGE4ZM2KrcdXLTQ9ViTp6pKRB1DLbpRPyEYzx4hQinvfZt058pQ7xoGdo+WDrAi98sD6GN6MM3jFLNJgkBZJxfttzzPz84FVER/UPH1VNIADYKfQXOfy1Nk95ZJebyIAaiy4D6VVTz7CG/nQ3QivDaovfkb1SvzRj668+iwo4qa32qETAa359y/vsZ78yimEWhT7ctfxeM5CtssqFhs79MYrqsfXUIYEHjiH6cxAQ7DJ7jlwzQ5ZEdGPfyFtvt/gtvjvlSCmX5x+Y1Xf6XyJ+pS/rzoleDbBKSZ2j+9OlrZ/DE5iZbXnqzeA+OLdjoyXAOk1SSuPsGjPPTDxUbm5+k1nrwQp4K7Dtw258CbT5bm/A01ZHCTa0xFKKMD9R3uyE1rcnyrm4JFAGOAs/fkMX8edkyftzC+qI8hFOLBujVnX1LgQlRmyZJ4Nz/slolk2s8buViJG2q1nURh2ZVt8JYOqKPN1Avjbmnc7Shem2XJbIwWdKwOPrUtKCsrBkb7DUXLx0lfcl3q95jtdp0gs0WpCS7j9+G7iOfOiVo5/XHFaDIoiPdJ9X27HDmVSe2IOH/vyK5zUBEpCKwo1IrR+QUwKhv1QrRIhQdAL69JEdOnPOes11mWdMo2UZQfLnOs0M1eT0hxLQjeuvQU/zG0OJu9G5+G8/u5u4zSCBQESJL7n6h/1barq9o03XP4KmwHjzBqPKiiezkDgCYoHUW50dKOflBr0LEH514H9wZ3P2TSQPTJWESGcfCAI9Etx+21FF6N9b1EpMFFJXY2fVz50Gs1iuAfn/4qTmfudNHW37oc/FIZfZ8Slm12//Ibfbfe1HrAJLG31Uk0C3YfEquKWsPNh8vZ43/lN5wuKNwyEnLF2GF4K1Qy/de/exTqXWTj2OpxKJ7in1ftan2ZJ1fxDM324uerglnqQmaiTSCiJ4QwsVjfFXETCw/7jsc4coSNrARGfYRF18srvfpx+iJia3EKOkWOZVw2MyQgjmKBf+HuxrJeJf6DzohFHtzK5vhZRP+WMPMe+QCo04WlOLYDVTD8Z6+qPjEwqfaRjnCIHC4ZyE+/e0aIHc7I69WKIO8Syme1Y6zO65BgFadeB06slyclHypldPLGzguT6WMFzbgtEd6IPU+LcVhOFQ4M6QcXX6UpDkMKHnZivKi5Ru03g0rDznBvyUiXQJ+JpSptpRim+tIYlk1GA3Gio2FuMUUCBqbu7LbWfLNhPB+efpY5B9RPT2HKZXB1HDROfsnySc/+hNz32dEcBEFl2kFEerwsAI1h0HYMXdbLWaze1xo6CUxkNsmyUuQMhvlTRXKQBEKE2sBhWlE3MAexaVO/Qsu69H9pEqZjV6oh/uVReswYsDdTGed0OYrL9TnLW5a0EJcJN0aMTo6x+gn+m3l3Xtj0Xqn+vZeIRXgllUY0hv2qtM8DUSYcR85obQPa3ssbzGzJFrYJci3uflui+kmEOu0wgfjwKzPW6GdtjysUo5m9zM+dExPLse7cqhcW4mb0qxvsFXz0rGiySeoUg3xdZSzaPx6XONhPjMbNbnwQrQTEafwStUH0BcMsTt6d0W8jvz820lqJQg4ctEEgV99waDlR54H074n9vztZV/73fchAomfMOp7m6+F8Kck93FjtHsHgNt3sv8pktA8QmnSOCEHVoCI9TKPNjp322DLGPGfrhrymgPzIfmfvXVGTyGSNF/ysHLK2E5kzGw6MQMk8sXV8V7kKS2s5UnBVFD9pAcweIGxv8AjAaAy+2OD7dvnGigKoMZQC8jvsgugkCVm9pQL8IFq39zxPTaU+vZuS5ADhsrPUQYstXgIipQKPiIBp3f9qUpH5wEgnmsC8ERmKwn6gQFrFgtJaAFdnBCabdAPuX+QdtJWczNyk4QI8KG8JXz7v4OXX60JvE+FupzAffJ8Zvgd8u4ejQp5HXjpIPdKXzmtz9JcDBoi8M9sU+lJhl7d8CP0SlS6n+dMLxM8Cgxj0GOL0xwE8I0e6SuDJdi8JcaRdLcdH/Ych9UKs5r9xa80a3hC8feFJS0XdHTBrhrKsaFuxymjYCs5pv7vz1W5JX+607vzJc3h+eRo1dsLKsPY7zXGjHzmKKCNLiSVJCRxtKZAg3Ny4AhfVVoP2iOlD+586VgJMnEzm+qZbGJoYq6AB5YqGMxEGT7lW49tWeAga58Cd1N4SBN5g5X7v3ONSyQSf7Q8IXs75+c1IHg3beA/dg==', + status: true, + comment: + 'file://ciphertexts/d88cfadc-4595-49a4-b38f-c3d01102c4ed from Python H-Keyring vectors', + }, + + { + ciphertext: + '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', + commitment: 'z8i9oh3LLQh7PLsPFKvSGT/JHf4vC4syz2TWdfC/9bw=', + 'content-encryption-key': 'Not tested', + 'decrypted-dek': 'Not tested', + exception: null, + header: + 'AgV42CSZ2CYjBDCvAR7JXsgiEjW2+P+OnIOtmHmzk3jIHBoAXwABABVhd3MtY3J5cHRvLXB1YmxpYy1rZXkAREFxQ21vWktzZVQvNXJJcVM0L3ZhQjBYRUQvZytkZUo0Z0gwYnBCN09PdXUvUkhmb2FZa3VHdGJzZ0paSXUvYjBTZz09AAEAEWF3cy1rbXMtaGllcmFyY2h5ACRiZDM4NDJmZi0zMDc2LTQwOTItOTkxOC00Mzk1NzMwMDUwYjgAXEIJx9Q5gGEsR8qcxkr2Sxx2ibsT+YEu9fp5kMrpzhij7bVCcp+GHKy3mX/2kZtEbEF6yspi57RrvZ25JhBlWiRHyN7Yxct1KZ7/fuyh8idHdiQApuG0obwbXuxcAgAAAgDPyL2iHcstCHs8uw8Uq9IZP8kd/i8LizLPZNZ18L/1vA==', + 'encryption-context': { + 'aws-crypto-public-key': + 'AqCmoZKseT/5rIqS4/vaB0XED/g+deJ4gH0bpB7OOuu/RHfoaYkuGtbsgJZIu/b0Sg==', + }, + 'keyring-type': 'static-branch-key', + 'message-id': '2CSZ2CYjBDCvAR7JXsgiEjW2+P+OnIOtmHmzk3jIHBo=', + plaintextBase64: + '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', + status: true, + comment: + 'file://ciphertexts/d88cfadc-4595-49a4-b38f-c3d01102c4ed from Python H-Keyring vectors', + }, + + { + ciphertext: + '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', + commitment: 'uqU2C2L1ck9rQTfu1XADX9BRFbEeTS8iqOWSp9C9A1o=', + 'content-encryption-key': 'Not tested', + 'decrypted-dek': 'Not tested', + exception: null, + header: + 'AgV45uctMxcoqp7GYZRgVvc5PAIZbhSOJBvESDIweAyWAU8AXwABABVhd3MtY3J5cHRvLXB1YmxpYy1rZXkAREFnQXFUbnNOakVjOVUvb3JsMzU1eGtGTVIydG5iMXhMRnMva1lUdDJaejdXckc3SmxBcHJsZWpreFBRYjlRRzI2dz09AAEAEWF3cy1rbXMtaGllcmFyY2h5ACRiZDM4NDJmZi0zMDc2LTQwOTItOTkxOC00Mzk1NzMwMDUwYjgAXFy9qdTe8/+qWPOwuLOdEeiz7Zfwuk2QcJxhKLDpzhij7bVCcp+GHKy3mX/2XTuEO/l80JrHaHiwSl31VAM8WDBSBkLGMrlLQNLsH+gnqr7AqLL1jtvE/oo1mNA4AgAAEAC6pTYLYvVyT2tBN+7VcANf0FEVsR5NLyKo5ZKn0L0DWg==', + 'encryption-context': { + 'aws-crypto-public-key': + 'AgAqTnsNjEc9U/orl355xkFMR2tnb1xLFs/kYTt2Zz7WrG7JlAprlejkxPQb9QG26w==', + }, + 'keyring-type': 'static-branch-key', + 'message-id': '5uctMxcoqp7GYZRgVvc5PAIZbhSOJBvESDIweAyWAU8=', + plaintextBase64: + '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', + status: true, + comment: 'First created from ESDK JS', + }, + { + ciphertext: + 'AgR48E+M4Ae1Fm++2AvvaDAkwHGWOZWbufpHYKQZl+pbrBcAAAABABFhd3Mta21zLWhpZXJhcmNoeQAkYmQzODQyZmYtMzA3Ni00MDkyLTk5MTgtNDM5NTczMDA1MGI4AFydjLLQc6/xNbMJIioj4HNk2ChHz968tNx3zi6E6c4Yo+21QnKfhhyst5l/9h+BPC6yJuopwwbQcb9RvW43D95GetUjzrugS+MFzvGjI4UmdS98IcqPXUQOcaoAUwIAABAAwERs7qbFokWayd+vuQep2pG6h54f4DEqR74HiDurF2jREdiI+k4n+bQvWBxh67u7AAAAAQAAAAAAAAAAAAAAAWs84xuV9ydQfvGTvjs/zZPwRwybBr76PVqDEQzhuy5H3WvMFzUrYODvbYSA2edJBQMMkr55agrtcZpSX12bFgyzyTgt/gZT9tG7jZO51kkJ9zl/7PrdzKMoSE4vtPpIQQTtIMs6vr87QIBHSFkYDNPnBdYX3LzWm4dFteCMjjGeIT1xllLzD//aoe3SH/bUSNc6OKIpZaGEFDN7SA764TqTk6KgCsltTm4umbN3UuQb2PGvF2LK/PuMAUe5VwwqIRW250E4DYT2g3K5bD2wci1lBYYhN9MDLCD9+0CUd6yRLh0bzwXV1UHgHePNqeErtDdCDkzRbKqaNbN96eCjdlW1g+DG8PMBu/q81ugF8A1x9ScO4mu3GjZT2mdb2Iy2KJ+07ltnICS/pXLiQ/Wn/Qub9euyZZ3Zseit7qO0KCwTqQzBWPShx7r42ndtTc06bUFVJOqSIqdqRT0AAtjrs57xeQyExMyD4ND3ZuqW72CxtQKPGkKAyQIbTa5ccnPNNoaBtT6isytt4h30GFe4aIj2mqDI3QAnEhl2SrLvOxua7IsY/YnV3ycIMpLgKaTDQVo3LC44BdmsqiLlv9jkHN8exL3MmoBnFz/gS1p0ps62yjIePJzYs7Wy/bHZEDwcAj7j05MPjPRKTiquj9N3T+wn7ZxgXG3eDPg7SV/+ph63ipdCgjg7P8WzdPago7DSl9Fo0XvAmf8RVt46acCeGcuNG1xDJtafDfSQ2z36FNQqYL1E6k3NZtRT3VWV+AqijBcUhBGLCGbHHLx5EXL5Y8Fzsgl2GHnEopMmdwCkDsl51Ouht9I3WAU+VtzC5pn9W5RlPIV01wtSyTe5qQnGcW7osY/7961ZB6e4CoT36B6gpuZvvn7nj6uzvG/XYWzZfSctpXIRAduIkgDO5MVxdasMhSu/Kh5ZGOJY+8Zry9uG0XHzsgs82QnxNQCYUrzP4GFKL/YBS0yjBfkVohFpEOGLmTX0IOYS9/bDCHO6c1m7ZmYQPwQ1Bn9Bc6DCOfUevP0i8cUKFTiNqQZKzX8x0Hu0KCnC4QoUtqLi5KuNazbZCsTVSjB9CiLZrgf9oHQu3AfHVX0Z5iGJs3cz+QzmSeSlBi3Z2kpB7S7RBaJVBPzIhHFeorWCg2kYvQQO75wty5+Ej3lnC8T9z/JHHcJEpXoArxl2hZd82oCPlaRRZ3q6WyISQBn0bJWvmWuzt/H0KEW6GxmWUIgAHZZA9eI8+aUahRIGd2E3ADgRZ97BDk/IG5qeMDwMV1SyUYjyWwlMtQwY6lDSOM4Huc7qRKSJece/0fIva5o66GU67H0ycQtLelp6L7dG5UjDnVOOZI7c1KA9DJiLvDU+DhYPFgNZ6cpT6Np+zVJ/v99Ns0AjN2jBEqSdtWTXMS2RH3uhjR91G0bK+nxTTM4L6GZMnWLvYkB+R46PVyzE+jQab6r0+b6YKHkxIVA4TJ7T7ROo7A0XLmvc62Nx0V48mN4hQ5W/oPPwN658ERe+N8w4Fk0F1ytzwkbtXmncImxhaetymfrmwogS1DCLKMuNv6Yzt8zmlTtsFq7foa6jxkCUsm5p10Obu7NIt/eOMt5S2fqUWEyTNS+mXCRDfdYJlaZQ2r81mhOPbPN2UcSY7G6WaW//NqK0fOQUKujcEdXD8C5InySjY0XghUpXSTa/3oQl5pZcp5w+NhBeBEJ4e4tXr3VKhK096DR+xIXpdAkUEZxhdD5GJKODgKb6qKIkZop4q0d++ZUsZ1892zSTbmAOYJR9GLfGJ33d9iVqmCj81GtHugHy23+G8R7vQB4DFhBVY0yHUpYF3wRkMTrQCs2O40aozo6OX0Wi1d+fup5Jddi30HNmftJOJQFo3W/cAz+FuHGFQp829OWyPTakw6yd1wBez86xCSlgca1sUnUtFQPkA+8mlbDGIFllisLf/5i4VQN11kk0M6PHx+yRopXS7aYxyzBwIY7QYA5iSc+PNQiCad2eZEbZmrfHKHLVZQnBoRCMyflp3F5BeeD/l5+DxTFl6rKVDQNkbc29vdLntK3b3s2GZDCYdtXkyVHkwER7gTluPsxbHgQAjVBCGGqIiGvnK+YHMxQfncZ2JpYVpiiqbXJLBNwxaThKxRfjRNFxFF55aqHTlyxsLZqECZZ9OdbcfZ8disWtGmm3I9/QUebicnO6MpN898tXA3gnJNXYaCdEuWeG2On9ONhiFaQ4u4K+R+87fwiBt5LqBQmRseFzm3HTBh1iVh8UgHudVBIl4yNFQUSKZ+7CWtX/M9WvFXMGsHFxOrzvdThDHUfvo57fGPwT/QpKCZwNp8W8LRNCVqA2BKlBx4YSHF0XpJYby8FmXPO6XKofPzBym8icLmrMU0+HW9IRTyyRO5eHVsFdgUnnrETjQdJP7BJ5ggdDltWPVStXPe+oibvVwICX1uMDRbMwN7L6ITDs/FREiVqlouclC8lbpy76Is+ju8EKgqr5qEPRno5Ecc0q4sGQHPc175Ilt3r5+LNR9Gqf9l2NmuP5iSLXwAqJsfAHzKS7xZ1d12pag4IoPLLb0oF9pvGTY6EiFtWtEjAe3xp0VI+ygVJnvGthgioXE89QU8q91Aqm5BKZ/mCsGEzCzf4FCDVQclRwqDHf8WUwcr/aEg6fSiKqNPf2XdIDQhZ0xQEXV6v/OXPge1dCM1mEHVA0LOkTK+ad4OH2GsdQnk/JjY+uQLWq/l3JJdFSfB0a9yYJZSk6fN1ISvSwZK4mGTBvMxzV1TsbbusIIFF0GVSrwXmSOxZO9OFgVgstq1DLqrc9mRbL+LFNBBr7azAdn+FIYT9IAEMPjxxHZnsA/WlKPyslRED7wbUaLE8l+AgesHsgIhMXC36D7XXURfwQ9LSK1LaEGYZrNuSiJUdjhvAyv1/DffOlu8w0PQ/CjDL90eHaioDX0BNmF5jIKJBTRsV5Qgf8nC3YjPqB6bCmYdmCGdkgxnwr/nrfeghPHaM1WR1eyDhFRKCtc3GC/Sk/V1/fRguHycGA2pmKUQb/k7CyIzEyGQ5PFkZ5Smys4DTzBoGsJF1lHUfARqhrLx5I+GB0FRd2qHQ0Vem8Ow6aL9GdEEEwZaR3WhnDjKydDsORxTPofKyvGW7hsGvi/foSJi5MUyZup3sZ8OziUEQb98nfRJVh6ArwqAYJ7YnzWCM0f2Y37svaOGYJPZ9S875EDHc+38SU/1BIWVfFJh17SOzrPAzL3ZaWkAhSq+rPWQoWDl3dCkIEVeCXV37Lyg5O1cXqYeu1ksfL0DrlzR6UnCGucxhhKSp/8FUFVGPASakq6uXPCSc53wRxJgVwQSAu3mC0KyA2UNlH9HjQZyWmRelPva6Eo4bNyWgfTrJJJKS/O9zq3y979Om6JYI7WRQDU7/oRFJ9mAZ8VOwVFj3uKzuWrm3lSCYF8AQYirVE8ZxSr9kuuTracqcnL9qEjMXMzyp0Wlx8Fsq4YC3SSicmFEl3mHrpTkURyAtpamainw6Qyb2bFBPVXXgvtGJgw3zW/SGUZPwwCXi305zgFa/Ij1QCG4BwZJi20l5a+QP+3RV6454UbM8LF1u/Sg92KFHWfobLCz9BZE0lUFM54zLoIRsXAAG0kZnGYPy5FTzWMHnA01fQaIv2fL0KUwEf21b5HIniqZAZLVqpt8N+HyRzGd8Tga89r2Cd7I345e5hFbW3axvUELz5GRS//P7m5R94DohCteMERFavlljz1f4cttYfY/N5N2z5Fw8NoLEEVQ6Hpwc6X5srmEEu5wpoND+vi93Lq8iQQd+B2W8Qc58jl0T+5xhRZdoeKUah0/94Nzpm26NYUM8Lmvd1TA2PkYFvIsXeNfoHnHbeLCXLue7F6CXb43Br/6ga+fAxrIbap5FFY60k/zgVN1XDmy73770hjIIplcN5+EKovohjpMEkJWGcZ3k6nMHdO4vouwCEeg3vu0sjWCzg4wiBj/mPyq1WKDfwipc+L9XLWaErvPdasgTZb61Y2IUVm3TegiiCqrqgNbFMH2yHGqFVbDKKBLeK5P9T411Y+F7jdcPzUVfmnd7r0qsC8ETBx96zzzmmdb+v37iMkK+kc5QoOGe6J3EgQg0PqCZsHQfc/O7LNqpCR5hm16/gxOhdx/Sq9+XpunQLIn0SLwIy/CjZkDOnVSRj4T+0+dq1aYHCbgbPgszfaZ/KI1nOmv1jaXXCHfMc1yJ+GOcElJPl76dlVkvNX2dUZiKRJUaLNZ16i1q34bGtKgs3DsyfTPaxLRcqmFPdgoI+185sJNseWBc6gQpZxf4YbSYvhKQ5cCanD6YOZD3qeFmn/KMYiddCx2osOayVUuywYR/C+xcx2zVZtVUWXv65GN9xV/6ZHBckwpMKMOKqUuAKvaxuS6LbOlnuCmjIzTLaePCLbZYgZcVvXOvGhKkkulfjWC0WqLPyqUOi0RQHifaTr/V+yLc2QuElWl4xhqPXloAo1HHZCaGYBEghALdYUnc8FG74tetEvqfgllGrol/bHeFRF4ZN3U8mXvEX6j+IOZD2QEE3KHwMYKWrQZospwdqkUrpJ7EWev6BYRe6/OtaaTjJpcXEHHjQpEDOtTfJcFP9H954WbpPIaSwZ4YUuYhLlAGlKb5QehCevwafz0u9/v5ElFfxmddvAYw6Lq5h86y9bRuEP787DJcLFpnVg2EIANg5c+MJI7JHJ4qu20RJGMqYkRRrK8Atu20juqb0hGVf5JXqho9dm9X3tTQZ5vkUs661JPCpWRDN/xgGAnYjUY1mbe7Bk0Hfbf4bsRqRSNCzNjDfl1Bh+WR1IJlNqXGRYAHPhK+n9u+7XsQdHyMi1hn1h/BC8QQO2NHs8Be3zjHhQjnr7JTMiAbDkZyS1Z45ytz+HnEvjejoqAGiLS18xScrhO4aKvqhXUVdGA1h97uBTq/ma7cYTplG70/8PMfzMERlDmPumZQCKO/a2IH351lXk5407j7GCREkzpHBrzeGO7Bcr3m+JrfmCLYJ0YkWS7J/1IhC9lK4KJrU74yofbKOoSrcDokByUahbGEeZr4Wc/DFruzoeeH8OYCZcVUdNGGWJRoxTHMcC3HQ1Zfx42HFaVQoiQVZdfUyNT0XuVPYBvZCtykz8h2ZV9TzFN6Md6cIZLZ1Re9qH/e9eYCLfMT6INuJydPalUJwbsEAi0hfdV6ShZA0sC0qCpwhH63Ner6i4XfK+TJP5nffQ7EDiLxFCJ/cSpndFRc/blvZoMlXCmex7oRmD6ESAhxz5ibOXphrFpWi+0ChzftkG36aGoWi72ACI+MXYJDN208ryZHbRWRhnBBWC9XxnuF9vDsXOBepe/iKlcBLOMgTDpfIuumNWGSLPwZRm+STIntOV4md1PUIcqMTvn8aLOhpJQPbHlAobY8CnFLSbgMufqoTqWRHSM+0aSUja16hqrN34DnL0DCxAAhJcJZdBTv6jLv6MXY75hCbKdWh+FHfhwG/kkvsayEZr3swqhPWBsKLOXYaQyEMq7pnAAAAAgAAAAAAAAAAAAAAAip1AJODB9bDqQaF5FQIDof0gmLWW/zryCnZhVLpbQmhGL7+qdz4Qi1ov2C3HrcZ4SdUetd+Ml6TZPgsz/Ka7B8I9ZdbvOZZ6kG7sUdBYcWR+7YzvOqyDMo8Mn+6z5MC/p1vCNZDvmCZRibNa3kZ/PMv7lJwu5N2Yyrl/j5NfuOAn/NJ+vvWxIIA3495cV+QUIx8TViz+kxv0VxxOuT0lnZyELRl7Vsk7Mk25g1dmk6qlKGSrv+q0gjy94PNmOSxgsP6prZmp7ZUWUiRGCXkQvOCrgqBoGtYcuVxJironrluORHhm7yMAi8wE9DDvLxRm3u9fwcajjNvLqJQmRtx6NH0O5echzdY/d2/ZJtH3XMPXoGWd2onmB7cUJt8USs6rjk9Gb3LuiP8Wu69cmM5xkIzRNM0o8rAOy4GFJTRZEQSMcQ4FWfTc0vRcrnO4NGtgPbtddv/EnmmIOgdWB4IbtnmjvpcCeammrjXZl22iKgh8ypvs2ITyCemgftboajknQU9JKpN488KNaQTPDnMhJvUaHrsXtCVP0B9bpHo2eAHu1+jV793+a/2Mv+t0+HrKOzusbp+r6rQ4rEp/ji40CoL/83bcAZLUhmhs/lYT7Cq6xOcCrXhxJn+oHDUmHkG938PH+NAZysgGseBgQrv3zrphSei37DhRSMOk9Nw1IVg7hkVTXWT5lelpOnmrraKrTo9+NAVksXBnW+a1iv0Ve0I+KqacvTnx301oqOOManarlj5SdNfYAfdwL9Kn4y5XTPFQW2+iKJ75/PTsGCUy6E3RxKx3cqAcG7VjIcOoexGDdKcC4kiC6uISzHxA2UI4Yaq0297OzyIdcTgBQ1riSkBxhXZuDRXMbECcYXUJZ3wpuamE12LV3jVMjltBpEH0BjPyd8UZRPCseOUjgFfZN6T3nW9p9d3pezO0UPzelK6voQK4loTdZF1gC45/5Pt4dY2rUOinLhw6nArIVYiUyretl5ejoArLutc5NxQ/+/Y7M2qrpvKbx/GVXyNU4GiWLCtKDpxI4KTs4Jqx3gG2eP5rK2Kdg6/dbj/duqWwhKWd2aUVrueZX+vXzRvSNMDP1xZvQgVxclJ4i7yo2lVRNtllRQ/R//aTnSw99zzEnMgMEFJzdz1xPr0VmpT103qb1F1z9VhHq7rldlXtYXHT0g4HgNIMC6OSdC3wrcS1u/2Sq/7BYx/BuFc8uj1hpUhHlGA5ceYn5SJJLlRIlxsZADKIuV7ZXYoZUji+bT9e8SPGTHlJQ4H+jq6N75uiihtv03DXOWuwW+ebkRB9rsaUYj54QadPegrbx0wmWWRYICGviItc6dltX1Z0yaHFRBGN/bkgA2cz5zEamrpGcEgsuYwe07MW78YxxUYTebNY6ku1V85PB8aqdTuEnDYkRjhbvGPrhdPu4o1J6hHPMAMDLjirpQ1Wd70d5FVr/Wu4ILcMLzNeKm+5s3sKkOlHUY3vh/xFXxJ2y8FW6yI4FHdXsfwVzYR4ET6om78OOl0wxLQw5MSPGvhJN4LPWLV5ZxAl42Ux55kQix4lOpf3J6i6lb4ZSM6ST2Sc9/ABKGwiP/pl2bFk5uRUu60RCQjwCHeFh9RD0G6Dz5IgFItItDpDmCMpJao9qJv8nozXroffL6lBBBbl8wYr44WkmfYh3Mqt91BW2Tdx9/kjUJw/93iyBXWZmfGRN4JZoVoZoixmpagO3z7SHE4icEUOKKeqhBltPrAGFKiYqas5Nj+3CRbeny36AxuNayzNfouEZUfDPk1Pao0kyuu/7wmTMk/0jACVVl4pXRF8S+M1jMLCOP1VPUVaipa6ZZ4/pa8eQbg7VJYul7t4pKLsfTC36IOzeztEN6akqHg7Y4puzrLiqf/ibMoqhdFVBykgyWR9A7iJhjcsCMV4TI9o9fR03MJPn0IbyFu8UjqPKwsNJR97R2zM2T5RKnnj0gapkK2UglCYQ9eXtBKAxwZBpN3/p+IPZRIv9pbQrfBouBJlDzxYueWW0aTgMYMsS6Euhj0XmGkYNyJ4HoWWl+9tIPDZsdlFDFLCxHghIGhy46dv+u6MchjLoIRLEgPPf1qoxyhq0isV+AZWGLBKtW9scDyvQidJc63kfhIvm0CPr6xBETM4mMCa2yalaBpYKNIB5YDnN5MFdP4a1sTPl4TQmHrhQH0FpBbB9kKQX/ynNBDc9GnHKsYFNymEzPohXNF1qa9QqmfCHR+4qLpzX3qmmwtSTZ/uZJdvlPvYnOWxKF2AgPhXDqI0/6upwsX1diodtTD20vvDuyibATIao3a8h5450frz+r9lhqRyzoH9gdMmub2ZYX3YFz09+2u1dedsogGsHyiP0Ru2HNz+xz65DzGq4U8ZljPUOROk9H2yQnkGFVFVfzRzxo+p4L1excrhGDLspiJJQVjpDqY31ioMUxlH0WgFQlFMiptok4uXwcXz3TEb+evRTfkvJG7XqmTVpAA1mIMi56O8Sj6f6J6OtvBHGObmEC2XQFd2BJSwYK6qpzr4rhMDNIY/xQAq5t445Ia2ujSTbnzEzoA68JO9nJ7hMRKqFTiblsj4oBkouqwD9ff+Vld9Ys11AVN6+WfY6l043FI5CmUm1XubgRAH1655///Q99kAhbRagHlb7QZWwxjN5odPEsgVaMQnw0mtWDN7z5hwO3lEdnsxqkBWCOXbkthDM9k+7/bA7Q9nxaenesdoYabiECXLho71bva1QVUuQJUF7Yu0Yynemlku3myqRtOm1vN/f7ra5wrsEyb4iZ4hMsokaUemdbBQbvgl/csh80fdVlmLfaKBkZHfxqwyqoLXvUjyG09wglgbjooBd6arA8yQhM/xP4/j1tY+ISDSYKGYD+mWIlAffhZmyQHxSeHnlkQ0GJeMaR6SItIaeahifyVIGOoVTw2sZzRkjSUVr62+ZvUEOhC4bGWzT7p1lijbC/RtFhmJ51aX7m6hLDofG/lfxRBkzJ9vq2ERTZZDw3CPxjYxWFPuY4HTYYcMV8p0WLxFXvl3pxpxRSDPBdF5bxoipwN7yzfrjBiQ2SrozqZgjxpVdaqj2HSYW0Mo8me9OMHRFksTiMYmkofbh+342S8jslfh/ktin40XOBi2kN94n8lRLNVeqc1GqLLATYooTqXkQT1s7NefFI+blo1XiM++wqU1kb80dDB9zmYdn2IvVNCA+Yx3dqIF7mq3U1wbjU8aPRoyXGU8iMHjsgwMKpC+dEmL3zYlM4EdHAOtDgJtqApbeCiq8rM49luKCT6qy6f4teBqiNd1nf8O93md0LjR+qurUbqWxI37Q/ymmcFuj2e66ZgtEyAHJm1KhAA07uwusGUY18nw5QFBYEPdeddh2zzCIAuTU5Rgg3YS1fQp7z6/TD5HNSGZPE55yQhTGgTO1G8ztXhoE9MczEvHiekWNUfX0iN6ZSrj/QF9A54TswTTH0e97jppajMW8BmlERvDBK404EaGvNQvbyxJC116NDm40lW8DUPXaA/eyCMpfbDOOW61YS05q6+jqITE7em3GKcyfheAocFMAKoPZLJypYxt4n29SqG7dn/ydElm23k4pZe+FCKXBeQezLZxG7SazAW89JMwLix6HnI2hJ8XDh+i8P3BLHS9Y6QMN/kmd7NrtDg3/UB8Dc8t5lYu912qMAkxDvXyyxmZZ4zOWed+wv5p4ZLhnwE0ZLqrp2hmGrWeuc/CKY/juhV33qzOqfgThPHPxDZA6cefCwbUlWoDfyTBnqyUIkAq2XkvEhPrQETKMCp24SONd7SNIWel8clTdOajZe0xYiUb5Tm+4LTNZpMo0LrejaN5zLSWuyf2lwgMDsrxU+IzKtSrFr4Dhorh34mvwA2ZnnuwEr1CAF+hc/82zheVlcoJEH9XweWNGW97/hRMYHS+msynStRa2kTcf6wKIJJfE5Zt8RVaX9XtiaoaccavXAUK4qY3uPBxm3OeNzs1RG12teMk4e0BazFKMB29U4GrHkJ1n0dxwBqTUs+FSveaBbjg7OSNCCJ/Or91MRdFT0IT7rxie2zRh815mlVD8gvx47jEfjb9muuECpAQCwzXG1hh0E/tYEXu6XpyBG8An4mdGiLcixX+e+UcSQ+/SP8bl7WKLzdHt5Juwl2+AlRhtEDncm7QcBg1nI6wFwsrNfBfWCBMeIQXQLWTiOLPQApNn/hjMRk7IdTS47SAyi4Bewbkdr/rICZgKY0Rg8xZAyLQq7D++WE/a1SIfjXr18fOUP2GRdZ4dDoEeU40EkN8lgvbHvWY2UoP1HpUi4ZIwzGSBFl6g37ynoaT3DSBSUx/4HcdkrJa93OBKupcM0wRs40LLFZSIja+jksU+f4SUUg1lr135T1V7/GEcsMWBXRYxW4MTcp/XGXCnP08ify17NWVBtYyG2yCZWLYKrIs5S6OO/OhfJ6cqLOu5cxCNS3qwsOYbS5c+0vhPTqcJ9vbSkjjcUBooJnaPzcyGL5kQyfSlkXnG89z95Y238c04E1OVx8FYUYZw2asvY9CY52dkGu1zu2po2YvHTlo4hhm1UFCN+Y7xB9Zc+FM86W2yuyTlqCANl9arpdvc4y5ZV1ZD32R3h6xdMac0ya8zqRidbLipib6SHzSWEkx4RZjJ+DAA51YujE1TRwYI2AWu8qACutEs040fRCvvaLmL2f1CaWMkGkepEEtdnNSAi3dxBvrgyVizdd+NAiS38b8lvihXhZxvOBGKdtE6YqLGHFMOltldH0Gn7jqMZpDdLDKGHl3A0K8lNljWUkbmb2/W/uoxG1knzJf1wGPMU0/u0MGBnIn3bvy6QtRBJHf7ZovhEkInx1xSMadTvmr+VV/TOO8pMCZWbgcGd1F26j3iaUWgE42TQItSz5ejzUo4N8jVRERtwPw3elKBquvQp03nG1WlycqmRKA5SRWJhjfsEOa9bTqa38o38OzEbGwn4X9lEGvMA2McnzXSH+6NL2IAyuNXT9KORLT4CwVAY+UyVDqQwYtBwnHeyGgLVr5ZA063SdP60zMZlcUDRewCUFJ0tJL+UJEQiG5918i5QVCA/rcULZRCAHyWjhSXVD3osTVmaSF5hntUQIgc3yP9jpSlRWxc9hujMkBMSTflL9o8+q3XzJUgj8gTQ3v79Z9Ap1V9LvLDbLcTGOXI93C1voLK3ORMeqsHJtSvw4LbMQCHFQztCcM6sF4r1rQw1zcnoia1ntNCcE0W5q5R/melhcYh5A56x/QaW0JfxBpzaCQvAS90W+ewOADGTRvO22xX1eY7qh8a5dA19mPt0U3qpz/yY102PMEpFJvBBwT74en7A8vijrfOEK+/BqWyqL/9IsPk1sTk4W5cYYEb9TruxYHSp0Sb21H106LfAOkbukafkxTX6vHdtbag+9UsGFhjiWv+/RKa5pOXwPU8AJXoVItqurvEguFjBfrOQqcKYC59o8N4BforiSKWRwK8n9gXXIl9/xOAVoJ/K4CKJ91AhDvZ4hYpEaf8xrxgP5jLDD/////wAAAAMAAAAAAAAAAAAAAAMAAAgAu2/4ak5DOVwm5iKFLXdDHYG9FBkCs2mhaQGr8juoaZOl3ftT8NtQM4XRucLxtBRsDYl0UIg88ctHPS7sUJ1zDbyTT+Et9Jx/Zh82FaMh1E2RZwpH9ZukxlDU1cA9I9l3Wn9WQ5uBZCHXF7OOKuvSZQLfnXTgQ2cUKZVA/C/FmZTd9xxUOIuRHDrDaeiL/ReY8lMilhMKBPPr+xYnI1q/sxQCkJOihA5ooQpG4OWuN677i85GClURrz6Tu9bjuk9qcssQJ95T3kgmGNtQ/Prfp5JDgIxOPFmI0t0B8NX2gv6FULw2BVc7Dxz884XXZbUezdqfpwGZD/kJj+gJ5JL7YoXyeBFBUXK5awoKhTqC+0U25evpzjMQmoo7e/q5/1WbVlNjwGtHf+iqU38RaKYRRvhrvdBr+5KW3qlRF255zKVhYwokOVczdVmmTIUw3kxGlUMH3nmareWacZPjEN7HwIctFkfRaxjs/cQqfzgw7zYLMkRq91ndiWAMJMxemdyMkaRQr3WXdP5BNAhC5CVPhP2YyDPHjRq+BVnnRjNbj8XjJWTZzpDAsU1ExLKVOHGH9OUTMtJ4fvvkiMEsIldXXeeMgHp8s7lrOAvN+zM+PdL9eObQNU8JHS6eUhqO/JmWej0ZAfjWnzsH47QEKN1Z6pmd/hxGSD8iyh/BrMjbpcFd6198klBo6MdTNGHDS/QQL28s2ZS8mIHMtuJuhwz55ON39x/6O4lzd2hqid/H86F6z0AieRwm1eX+GLvXaVqScnoztrm2NaUFHeu9dEPW16ulK8FTQyBdbTHW0FZ3I0MYOg0QEeD4Q1/IwdXUNRFtOft6sG3aI6VpO45n5aA+JP8NInTO8TI0P0ICfVKcvO//WZ+8DI1ygXjfpsUNq0CQlb8GHal9BylxtoY57ny/OC4Zz9mEl9dD9QELxe3VVcH5TDn04yjKv2GTN0Je43tXfYrUWKMb93SNSzIB63eDHy4esiT2vjZQZrDJSORH2tzgWPoEcrbzHzggAw5jdwQ4AyEKRuNU7NEsO5F0try19XJj6dPQtxx18xYx1ZkAvI+Lu0SAEIFB7u6zkqa/FMVsL8WhacL8gm/dToJ+k/TrCE0ww5JYVjknE4f2HLvdsYMPG9bQHAFhrkId9dkfhqimLnXLTzPmFNvCQfTgYNX8iW811FT4KWvLRLNA3t/v/K4dJFVNVByjOIaaeADjqntoPlOUF+qtqYiw6/LRlqVpmfryj6psQavlAfc2IsTwrgvhtcsNjJMfo+xcE38ehSw+OAZlWREbxjvxx7DvVaiF1Q5vC5hdbfIkA8SKxEvajd0ClSt6gqI4xiDQMWBTtghJrhbwtpDcTD7gIqqtw1aJExj6yxnSu55jIPWxxfrD1vaI8CEt3bm6ZvNAUUOjQP09CzN54PpKy1f9kxMg7j+zgFN+XlFj3aHV9Ls6eTLfO1YgESCQ3VpByuWZClv6Fgkp6iw+qCB2IUgQ8iyA2X+Wu2cy8yP2wME/WOsd8dGSAK03ALqB1n37/D622244D8LMQCm4Hr9zlV86BdaLXYLnT2N4YthDMaHmqljZPZtjGmVUX3MMcFrqDzgYuzCu41IA3ym9YptUFuTF6ESuVHq39X2KsRE4CS6qw1j/a+5mpv6i5h9maUeUPpc8a8y19QeCT2TLFmSa49pGnvkSMZVL1o73XaaCHlMUzc9jSUAwNihJhjsWCD1lMDn0X8nfcMGMPldUYwqQDsORHTLrF+J8musHCO5GD1rZLvxkDSEj+FwaQ7/XX09Efuha4MJbcE8BwRVrSo0WvF0eIi5KLcHdVck8025MkDBF2sAoDkIvgXvoFYqazrD2j3LI+Sho42GyARo5duR0saIHm9CiQKOtNUe5hFtCMqiTmSdeJeK7N16niqB0enTBvwFn/WF9rryuBOQNEesKnUQsYXDWqo8RzUwBTdpdSLeS73v6GYcLijrN+qAfyTH7DRzXGg3xAcfscutTA4edicPKwd6L/tIt+oFEkVVQwv7r2BG17lZXjhHWVaQiKJXqoVikuJqnJloIhgsmnfqRyi4dZYwm62E58jq8S2X6p41RNDnPNbEjjlfV0cgh6ftYw0lvUkARUGxVZyB/Sh2WxRMiFj/lRKkUwIOT6mVKoQqNGfGQL9dNRaMqFCPcZwI1JpcJ4Rby9LJ9bBQLgVtal74/IU8R2Ui3EdWWeM3uSYII8yq9fyiPURFgOqh277JUW3wjciHMRiGnuhX/kcX5nbMKxpsD04gYby86jREuP+zeSqir5oJJEOUJixFa0vCxdlcFfZ49+5DKR+nMKRZG3cBh8owL1KgVTdn850fKuKWlE/EwxbcUF1X+g+WM9yDIHunauftkz7yoqokfDTMZ5qnbr5FvusUFhWlcF2JTffU/eVN79GUiBqYt0y53UK1gpfgGkpbzIUqw0FKua7q3S4wMdWzUGus1xYjXAHLKtvlsMEzHnRERtmgR2816RjNPTyPaI2cZJ4Oyp7fXLaBZyEkFligIUvv60Bmedhsk6xiHmaDHPKQ1GUcNeWVKy3zLCsaD+ktJH8s+ryziG+6SktANtg1Mvv3GYUEzTVjWWJMYopx1YsjuGVd0hJzxvorUSxlE917y2gVRKlJ18NfDl9OsOZBatNHAW+1dHCF0uI9VQeSEbJ29s+V4PDSWb1uQAZCDqqh7p3lBDDVqGnSNaj09e++h6MCYhAlkwG6fORXipa9iFYb2/TQZDkYqzNYPgGqrEQyeaNRP', + commitment: 'wERs7qbFokWayd+vuQep2pG6h54f4DEqR74HiDurF2g=', + 'content-encryption-key': 'Not tested', + 'decrypted-dek': 'Not tested', + exception: null, + header: + 'AgR48E+M4Ae1Fm++2AvvaDAkwHGWOZWbufpHYKQZl+pbrBcAAAABABFhd3Mta21zLWhpZXJhcmNoeQAkYmQzODQyZmYtMzA3Ni00MDkyLTk5MTgtNDM5NTczMDA1MGI4AFydjLLQc6/xNbMJIioj4HNk2ChHz968tNx3zi6E6c4Yo+21QnKfhhyst5l/9h+BPC6yJuopwwbQcb9RvW43D95GetUjzrugS+MFzvGjI4UmdS98IcqPXUQOcaoAUwIAABAAwERs7qbFokWayd+vuQep2pG6h54f4DEqR74HiDurF2g=', + 'encryption-context': {}, + 'keyring-type': 'static-branch-key', + 'message-id': '8E+M4Ae1Fm++2AvvaDAkwHGWOZWbufpHYKQZl+pbrBc=', + plaintextBase64: + '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', + status: true, + comment: 'Second created from ESDK JS', + }, + ], + } +} + +// This is some helpful scripts for pulling apart a test vector +// Leaving it here in case this needs to be done again. + +// import { deserializeFactory } from '@aws-crypto/serialize' +// import { readFileSync, writeFileSync } from 'fs' +// import { NodeAlgorithmSuite } from '@aws-crypto/material-management-node' + +// const filename = 'file-8E+M4Ae1Fm++2AvvaDAkwHGWOZWbufpHYKQZl+pbrBc=' +// const pathToManifiest = './' +// const someVector = readFileSync(`${pathToManifiest}ciphertexts/${filename}`) + +// const toUtf8 = (input: Uint8Array) => +// Buffer.from(input.buffer, input.byteOffset, input.byteLength).toString('utf8') +// const deserialize = deserializeFactory(toUtf8, NodeAlgorithmSuite) +// const qwer = deserialize.deserializeMessageHeader(someVector) + +// if (!!qwer && qwer.messageHeader.version == 2) { +// console.log('messaageID', qwer.messageHeader.messageId.toString('base64')) +// console.log('rawHeader', qwer.rawHeader.toString('base64')) +// console.log('commitement', qwer.messageHeader.suiteData.toString('base64')) +// writeFileSync(filename, someVector.toString('base64')) +// } 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 index 0594bf3a6..46e83c2bb 100644 --- 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 @@ -131,7 +131,6 @@ describe('AwsKmsMrkAwareSymmetricDiscoveryKeyringNode can encrypt/decrypt with A NumberOfBytes: suite.keyLengthBytes, EncryptionContext: encryptionContext, }) - console.log(CiphertextBlob) needs(CiphertextBlob instanceof Uint8Array, 'never') const edk = new EncryptedDataKey({ providerId: 'aws-kms', From b7e9745a011292661ac6411302af6722d1eb074f Mon Sep 17 00:00:00 2001 From: seebees Date: Mon, 13 Jan 2025 16:31:34 -0800 Subject: [PATCH 18/25] lint --- .../decrypt-node/test/compatibility.test.ts | 149 +++++++++--------- 1 file changed, 72 insertions(+), 77 deletions(-) diff --git a/modules/decrypt-node/test/compatibility.test.ts b/modules/decrypt-node/test/compatibility.test.ts index 4d5336d38..7ab6847cb 100644 --- a/modules/decrypt-node/test/compatibility.test.ts +++ b/modules/decrypt-node/test/compatibility.test.ts @@ -31,13 +31,6 @@ import { } from '@aws-crypto/kms-keyring-node' import { BranchKeyStoreNode } from '@aws-crypto/branch-keystore-node' -import { deserializeFactory } from '@aws-crypto/serialize' -import { NodeAlgorithmSuite } from '@aws-crypto/material-management-node' -import { readFileSync, writeFileSync } from 'fs' -const toUtf8 = (input: Uint8Array) => - Buffer.from(input.buffer, input.byteOffset, input.byteLength).toString('utf8') -const deserialize = deserializeFactory(toUtf8, NodeAlgorithmSuite) - const { decrypt } = buildDecrypt(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) const { encrypt } = buildEncrypt(CommitmentPolicy.REQUIRE_ENCRYPT_ALLOW_DECRYPT) @@ -81,7 +74,9 @@ describe('committing algorithm test', () => { needs(status, 'Unexpected Status') needs(plaintextBase64, 'Nothing to encrypt') - const suiteId = AlgorithmSuiteIdentifier.ALG_AES256_GCM_IV12_TAG16_HKDF_SHA512_COMMIT_KEY + const suiteId = once + ? AlgorithmSuiteIdentifier.ALG_AES256_GCM_IV12_TAG16_HKDF_SHA512_COMMIT_KEY + : AlgorithmSuiteIdentifier.ALG_AES256_GCM_IV12_TAG16_HKDF_SHA512_COMMIT_KEY_ECDSA_P384 once = true const encryptOutput = await encrypt(keyring, plaintextBase64, { @@ -128,14 +123,16 @@ describe('committing algorithm test', () => { case 'aws-kms': return new KmsKeyringNode({ discovery: true }) case 'static': - const dataKey = Buffer.alloc(32, test['decrypted-dek'], 'base64') - return new (class TestKeyring extends KeyringNode { async _onEncrypt(): Promise { throw new Error('I should never see this error') } async _onDecrypt(material: NodeDecryptionMaterial) { - const unencryptedDataKey = dataKey + const unencryptedDataKey = Buffer.alloc( + 32, + test['decrypted-dek'], + 'base64' + ) const trace = { keyNamespace: 'k', keyName: 'k', @@ -150,79 +147,77 @@ describe('committing algorithm test', () => { // This is *NOT* recommended. // The proper extension point for the KeyStore is _only_ the Storage interface! // However, this does let us do some quick test vector testing. - // At this time this is overly perscriptive, - // but the expectation is to be able to depracate this + // At this time this is overly prescriptive, + // but the expectation is to be able to deprecate this // in favor of the test vectors project (integration-node) - const keyStore = { - __proto__: BranchKeyStoreNode.prototype, - kmsConfiguration: { - getRegion() { - return null + return new KmsHierarchicalKeyRingNode({ + branchKeyId: 'bd3842ff-3076-4092-9918-4395730050b8', + cacheLimitTtl: 1, + keyStore: { + __proto__: BranchKeyStoreNode.prototype, + kmsConfiguration: { + getRegion() { + return null + }, }, - }, - - getKeyStoreInfo() { - return { - logicalKeyStoreName: 'logicalKeyStoreName', - } - }, - - async getBranchKeyVersion( - branchKeyId: string, - branchKeyVersion: string - ): Promise { - needs( - branchKeyId == 'bd3842ff-3076-4092-9918-4395730050b8', - branchKeyId - ) - needs( - branchKeyVersion == 'e9ce18a3-edb5-4272-9f86-1cacb7997ff6', - branchKeyVersion - ) - - return new NodeBranchKeyMaterial( - Buffer.from( - 'tJwf65epYvUt5HMiQsl/6jlvLxS0tgdjIuvFy2BLIwg=', - 'base64' - ), - branchKeyId, - branchKeyVersion, - {} - ) - }, - async getActiveBranchKey( - branchKeyId: string - ): Promise { - needs( - branchKeyId == 'bd3842ff-3076-4092-9918-4395730050b8', - branchKeyId - ) - - return new NodeBranchKeyMaterial( - Buffer.from( - 'tJwf65epYvUt5HMiQsl/6jlvLxS0tgdjIuvFy2BLIwg=', - 'base64' - ), - branchKeyId, - 'e9ce18a3-edb5-4272-9f86-1cacb7997ff6', - {} - ) - }, - storage: { - _config: {}, - getKeyStorageInfo() { + getKeyStoreInfo() { return { - logicalName: 'logicalKeyStoreName', + logicalKeyStoreName: 'logicalKeyStoreName', } }, - }, - } as any - return new KmsHierarchicalKeyRingNode({ - branchKeyId: 'bd3842ff-3076-4092-9918-4395730050b8', - keyStore, - cacheLimitTtl: 1, + async getBranchKeyVersion( + branchKeyId: string, + branchKeyVersion: string + ): Promise { + needs( + branchKeyId == 'bd3842ff-3076-4092-9918-4395730050b8', + branchKeyId + ) + needs( + branchKeyVersion == 'e9ce18a3-edb5-4272-9f86-1cacb7997ff6', + branchKeyVersion + ) + + return new NodeBranchKeyMaterial( + Buffer.from( + 'tJwf65epYvUt5HMiQsl/6jlvLxS0tgdjIuvFy2BLIwg=', + 'base64' + ), + branchKeyId, + branchKeyVersion, + {} + ) + }, + async getActiveBranchKey( + branchKeyId: string + ): Promise { + needs( + branchKeyId == 'bd3842ff-3076-4092-9918-4395730050b8', + branchKeyId + ) + + return new NodeBranchKeyMaterial( + Buffer.from( + 'tJwf65epYvUt5HMiQsl/6jlvLxS0tgdjIuvFy2BLIwg=', + 'base64' + ), + branchKeyId, + 'e9ce18a3-edb5-4272-9f86-1cacb7997ff6', + {} + ) + }, + + storage: { + _config: {}, + getKeyStorageInfo() { + return { + logicalName: 'logicalKeyStoreName', + } + }, + }, + } as any, }) } From 7fb3bba573b876c95809e0cce76555f82ce1a34d Mon Sep 17 00:00:00 2001 From: seebees Date: Tue, 14 Jan 2025 15:13:16 -0800 Subject: [PATCH 19/25] adding complete test vectors --- modules/kdf-ctr-mode-node/src/kdfctr.ts | 26 +- modules/kdf-ctr-mode-node/test/fixtures.ts | 974 ++++++++++++++++-- modules/kdf-ctr-mode-node/test/kdfctr.test.ts | 116 ++- 3 files changed, 993 insertions(+), 123 deletions(-) diff --git a/modules/kdf-ctr-mode-node/src/kdfctr.ts b/modules/kdf-ctr-mode-node/src/kdfctr.ts index 722755bc9..58835d459 100644 --- a/modules/kdf-ctr-mode-node/src/kdfctr.ts +++ b/modules/kdf-ctr-mode-node/src/kdfctr.ts @@ -14,11 +14,13 @@ import { uInt32BE } from '@aws-crypto/serialize' const SEPARATION_INDICATOR = Buffer.from([0x00]) const COUNTER_START_VALUE = 1 export const INT32_MAX_LIMIT = 2147483647 -const SUPPORTED_DERIVED_KEY_LENGTHS = [32] +const SUPPORTED_IKM_LENGTHS = [32, 48, 66] +const SUPPORTED_NONCE_LENGTHS = [16, 32] +const SUPPORTED_DERIVED_KEY_LENGTHS = [32, 64] const SUPPORTED_DIGEST_ALGORITHMS = ['sha256', 'sha384'] export type SupportedDigestAlgorithms = 'sha256' | 'sha384' -export type SupportedDerivedKeyLengths = 32 +export type SupportedDerivedKeyLengths = 32 | 64 interface KdfCtrInput { digestAlgorithm: SupportedDigestAlgorithms @@ -35,9 +37,20 @@ export function kdfCounterMode({ purpose, expectedLength, }: KdfCtrInput): Buffer { + + /* Precondition: the ikm must be 32, 48, 66 bytes long */ + needs( + SUPPORTED_IKM_LENGTHS.includes(ikm.length), + `Unsupported IKM length ${ikm.length}` + ) /* Precondition: the nonce is required */ needs(nonce, 'The nonce must be provided') - /* Precondition: the expected length must be 32 bytes */ + /* Precondition: the nonce must be 16, 32 bytes long */ + needs( + SUPPORTED_NONCE_LENGTHS.includes(nonce.length), + `Unsupported nonce length ${nonce.length}` + ) + /* Precondition: the expected length must be 32, 64 bytes */ /* Precondition: the expected length * 8 must be under the max 32-bit signed integer */ needs( SUPPORTED_DERIVED_KEY_LENGTHS.includes(expectedLength) && @@ -47,7 +60,7 @@ export function kdfCounterMode({ ) const label = purpose || Buffer.alloc(0) - const info = nonce || Buffer.alloc(0) + const info = nonce const internalLength = 8 + SEPARATION_INDICATOR.length /* Precondition: the input length must be under the max 32-bit signed integer */ @@ -102,11 +115,12 @@ export function rawDerive( ) // number of iterations calculated in accordance with SP800-108 - const iterations = Math.ceil(length / h) + const iterations = Math.floor((length + h - 1) / h) + let buffer = Buffer.alloc(0) let i = Buffer.from(uInt32BE(COUNTER_START_VALUE)) - for (let iteration = 1; iteration <= iterations + 1; iteration++) { + for (let iteration = 1; iteration <= iterations; iteration++) { const digest = createHmac(digestAlgorithm, ikm) .update(i) .update(explicitInfo) diff --git a/modules/kdf-ctr-mode-node/test/fixtures.ts b/modules/kdf-ctr-mode-node/test/fixtures.ts index 1f09698c5..a5bb6b5b8 100644 --- a/modules/kdf-ctr-mode-node/test/fixtures.ts +++ b/modules/kdf-ctr-mode-node/test/fixtures.ts @@ -19,7 +19,7 @@ interface InternalTestVector { okm: Buffer } -interface TestVector extends InternalTestVector { +export interface TestVector extends InternalTestVector { purpose: Buffer L: SupportedDerivedKeyLengths } @@ -325,117 +325,883 @@ const c5: TestVector = { // These sizes come from asking Dafny to generate tests for this code. // The highlights were strange lengths of buffers, and expected key length. // Dafny found that the interesting `L` was 2147482807. -// Way to huge to be practical, and JS limits us to 32... -// So these focus on -const t1: TestVector = { - name: '256 with empty info/nonce and purpose', - hash: 'sha256', - ikm: Buffer.from([ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - ]), - info: Buffer.from([]), - purpose: Buffer.from([]), - L: 32, - okm: Buffer.from([ - 131, 81, 40, 59, 15, 210, 28, 71, 12, 87, 94, 236, 126, 33, 233, 108, 95, - 149, 146, 127, 4, 11, 241, 34, 234, 165, 22, 19, 114, 10, 141, 187, - ]), -} +// So we test every combination of the test vectors input with 0s: +// Hash 256, 384 +// IKM 32, 48, 66 +// nonce 16, 32 +// L 32 64 +// purpose empty some -const t2: TestVector = { - name: '256 with empty purpose', - hash: 'sha256', - ikm: Buffer.from([ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - ]), - info: Buffer.from([0, 0, 0]), - purpose: Buffer.from([]), - L: 32, - okm: Buffer.from([ - 162, 137, 147, 225, 21, 174, 44, 20, 121, 29, 226, 152, 146, 76, 195, 64, - 174, 149, 122, 67, 111, 209, 67, 10, 87, 112, 245, 42, 14, 133, 247, 131, - ]), -} +// 2 * 3 * 2 * 2 * 2 == 48 -const t3: TestVector = { - name: '256 with empty info/nonce', - hash: 'sha256', - ikm: Buffer.from([ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - ]), - info: Buffer.from([]), - purpose: Buffer.from([0, 0, 0]), - L: 32, - okm: Buffer.from([ - 162, 137, 147, 225, 21, 174, 44, 20, 121, 29, 226, 152, 146, 76, 195, 64, - 174, 149, 122, 67, 111, 209, 67, 10, 87, 112, 245, 42, 14, 133, 247, 131, - ]), -} +const permutedVectors: TestVector[] = [ + { + name: '256 66 ikm 16 info empty purpose 32 L', + hash: 'sha256', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + purpose: Buffer.from([]), + L: 32, + okm: Buffer.from([ + 65, 81, 193, 63, 209, 164, 150, 11, 109, 207, 92, 45, 90, 68, 135, 42, 123, 180, + 253, 81, 14, 247, 137, 101, 193, 167, 240, 151, 189, 236, 116, 145 + ]), + }, + { + name: '256 66 ikm 32 info empty purpose 32 L', + hash: 'sha256', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ]), + purpose: Buffer.from([]), + L: 32, + okm: Buffer.from([ + 63, 229, 35, 81, 172, 145, 120, 135, 156, 61, 161, 195, 145, 204, 129, 113, 88, + 159, 215, 249, 28, 99, 136, 50, 228, 209, 246, 7, 231, 2, 57, 5 + ]), + }, + { + name: '256 66 ikm 16 info some purpose 32 L', + hash: 'sha256', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + purpose: Buffer.from([0, 0, 0]), + L: 32, + okm: Buffer.from([ + 98, 2, 240, 78, 90, 169, 82, 88, 245, 16, 130, 24, 0, 235, 127, 119, 7, 81, 31, + 246, 185, 49, 55, 210, 90, 80, 24, 70, 110, 41, 80, 231 + ]), + }, + { + name: '256 66 ikm 32 info some purpose 32 L', + hash: 'sha256', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ]), + purpose: Buffer.from([0, 0, 0]), + L: 32, + okm: Buffer.from([ + 97, 53, 70, 12, 112, 51, 12, 163, 48, 38, 248, 168, 7, 126, 186, 238, 152, 50, + 40, 209, 180, 179, 172, 51, 36, 67, 137, 82, 243, 93, 201, 20 + ]), + }, + { + name: '256 66 ikm 16 info empty purpose 64 L', + hash: 'sha256', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + purpose: Buffer.from([]), + L: 64, + okm: Buffer.from([ + 29, 73, 171, 201, 120, 179, 112, 19, 246, 140, 115, 142, 46, 127, 229, 46, 72, + 181, 250, 14, 146, 240, 162, 52, 225, 209, 224, 101, 40, 144, 174, 233, 202, + 251, 94, 49, 78, 238, 46, 141, 66, 87, 39, 122, 48, 67, 11, 43, 132, 139, 127, + 9, 146, 233, 15, 96, 169, 161, 238, 106, 15, 98, 250, 100 + ]), + }, + { + name: '256 66 ikm 32 info empty purpose 64 L', + hash: 'sha256', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ]), + purpose: Buffer.from([]), + L: 64, + okm: Buffer.from([ + 168, 235, 151, 41, 93, 127, 145, 9, 55, 72, 50, 39, 48, 9, 25, 117, 113, 153, + 194, 226, 105, 208, 44, 28, 23, 144, 20, 196, 52, 50, 174, 65, 250, 186, 247, + 162, 147, 155, 66, 63, 129, 179, 206, 216, 220, 160, 58, 52, 114, 87, 169, 239, + 244, 163, 51, 247, 41, 210, 158, 216, 128, 70, 222, 230 + ]), + }, + { + name: '256 66 ikm 16 info some purpose 64 L', + hash: 'sha256', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + purpose: Buffer.from([0, 0, 0]), + L: 64, + okm: Buffer.from([ + 12, 94, 33, 17, 175, 237, 91, 177, 171, 116, 128, 189, 58, 60, 35, 70, 7, 12, + 53, 47, 217, 145, 25, 179, 69, 54, 162, 177, 226, 48, 137, 2, 72, 233, 22, 20, + 25, 82, 234, 247, 45, 14, 65, 80, 16, 14, 133, 64, 128, 253, 210, 84, 181, 13, + 68, 110, 118, 45, 105, 247, 120, 4, 57, 97 + ]), + }, + { + name: '256 66 ikm 32 info some purpose 64 L', + hash: 'sha256', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ]), + purpose: Buffer.from([0, 0, 0]), + L: 64, + okm: Buffer.from([ + 133, 88, 230, 171, 118, 242, 195, 174, 144, 189, 243, 255, 178, 76, 43, 19, 194, + 230, 127, 121, 105, 112, 149, 112, 136, 24, 142, 152, 72, 52, 50, 69, 143, 28, + 169, 139, 172, 17, 203, 177, 172, 97, 93, 185, 90, 95, 2, 194, 204, 207, 200, + 246, 134, 217, 205, 160, 114, 77, 166, 84, 132, 196, 215, 127 + ]), + }, + { + name: '256 48 ikm 16 info empty purpose 32 L', + hash: 'sha256', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + purpose: Buffer.from([]), + L: 32, + okm: Buffer.from([ + 174, 225, 122, 80, 241, 5, 205, 225, 176, 135, 15, 182, 95, 87, 111, 246, 179, + 190, 55, 74, 187, 21, 81, 32, 218, 198, 229, 39, 214, 223, 52, 110 + ]), + }, + { + name: '256 48 ikm 32 info empty purpose 32 L', + hash: 'sha256', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ]), + purpose: Buffer.from([]), + L: 32, + okm: Buffer.from([ + 120, 20, 205, 190, 36, 210, 128, 96, 114, 37, 231, 140, 178, 104, 29, 88, 17, + 78, 64, 239, 106, 201, 187, 236, 162, 228, 238, 246, 2, 253, 21, 98 + ]), + }, + { + name: '256 48 ikm 16 info some purpose 32 L', + hash: 'sha256', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + purpose: Buffer.from([0, 0, 0]), + L: 32, + okm: Buffer.from([ + 50, 227, 144, 224, 232, 121, 50, 114, 63, 144, 192, 211, 168, 146, 64, 210, 94, + 59, 133, 30, 236, 141, 125, 28, 54, 110, 201, 185, 213, 40, 164, 253 + ]), + }, + { + name: '256 48 ikm 32 info some purpose 32 L', + hash: 'sha256', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ]), + purpose: Buffer.from([0, 0, 0]), + L: 32, + okm: Buffer.from([ + 232, 223, 75, 187, 68, 128, 68, 9, 48, 11, 238, 200, 238, 127, 249, 164, 39, + 134, 106, 134, 210, 139, 213, 242, 126, 92, 104, 242, 184, 52, 29, 156 + ]), + }, + { + name: '256 48 ikm 16 info empty purpose 64 L', + hash: 'sha256', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + purpose: Buffer.from([]), + L: 64, + okm: Buffer.from([ + 51, 160, 206, 70, 126, 207, 132, 181, 69, 177, 200, 56, 41, 184, 112, 236, 248, + 118, 1, 83, 102, 124, 10, 195, 119, 18, 79, 245, 95, 162, 181, 56, 81, 236, 177, + 189, 206, 96, 201, 81, 175, 224, 226, 29, 145, 43, 200, 85, 39, 114, 11, 109, + 149, 95, 25, 11, 115, 33, 203, 94, 254, 126, 91, 130 + ]), + }, + { + name: '256 48 ikm 32 info empty purpose 64 L', + hash: 'sha256', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ]), + purpose: Buffer.from([]), + L: 64, + okm: Buffer.from([ + 52, 56, 249, 137, 118, 241, 81, 3, 106, 148, 187, 233, 144, 50, 94, 163, 67, + 205, 109, 134, 183, 190, 143, 196, 219, 244, 120, 193, 244, 189, 86, 158, 109, + 109, 248, 231, 200, 58, 81, 17, 125, 98, 241, 66, 160, 75, 198, 179, 152, 75, + 78, 124, 238, 111, 9, 61, 193, 16, 48, 103, 202, 38, 129, 54 + ]), + }, + { + name: '256 48 ikm 16 info some purpose 64 L', + hash: 'sha256', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + purpose: Buffer.from([0, 0, 0]), + L: 64, + okm: Buffer.from([ + 149, 140, 56, 1, 42, 245, 141, 18, 147, 234, 181, 53, 117, 134, 205, 36, 207, + 162, 134, 79, 181, 46, 106, 91, 151, 77, 66, 248, 56, 48, 162, 103, 1, 93, 196, + 85, 14, 201, 194, 108, 225, 190, 80, 221, 10, 156, 15, 109, 79, 39, 93, 240, + 17, 198, 82, 18, 26, 230, 140, 175, 200, 70, 243, 73 + ]), + }, + { + name: '256 48 ikm 32 info some purpose 64 L', + hash: 'sha256', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ]), + purpose: Buffer.from([0, 0, 0]), + L: 64, + okm: Buffer.from([ + 246, 171, 10, 194, 229, 245, 91, 201, 41, 183, 21, 176, 40, 248, 85, 39, 158, + 233, 162, 152, 83, 192, 172, 239, 30, 97, 78, 140, 54, 168, 77, 243, 120, 159, + 194, 134, 42, 21, 121, 100, 184, 90, 95, 88, 14, 185, 208, 204, 216, 8, 65, 173, + 63, 200, 95, 76, 110, 57, 165, 239, 184, 191, 32, 129 + ]), + }, + { + name: '256 32 ikm 16 info empty purpose 32 L', + hash: 'sha256', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + purpose: Buffer.from([]), + L: 32, + okm: Buffer.from([ + 174, 225, 122, 80, 241, 5, 205, 225, 176, 135, 15, 182, 95, 87, 111, 246, 179, + 190, 55, 74, 187, 21, 81, 32, 218, 198, 229, 39, 214, 223, 52, 110 + ]), + }, + { + name: '256 32 ikm 32 info empty purpose 32 L', + hash: 'sha256', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ]), + purpose: Buffer.from([]), + L: 32, + okm: Buffer.from([ + 120, 20, 205, 190, 36, 210, 128, 96, 114, 37, 231, 140, 178, 104, 29, 88, 17, + 78, 64, 239, 106, 201, 187, 236, 162, 228, 238, 246, 2, 253, 21, 98 + ]), + }, + { + name: '256 32 ikm 16 info some purpose 32 L', + hash: 'sha256', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + purpose: Buffer.from([0, 0, 0]), + L: 32, + okm: Buffer.from([ + 50, 227, 144, 224, 232, 121, 50, 114, 63, 144, 192, 211, 168, 146, 64, 210, 94, + 59, 133, 30, 236, 141, 125, 28, 54, 110, 201, 185, 213, 40, 164, 253 + ]), + }, + { + name: '256 32 ikm 32 info some purpose 32 L', + hash: 'sha256', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ]), + purpose: Buffer.from([0, 0, 0]), + L: 32, + okm: Buffer.from([ + 232, 223, 75, 187, 68, 128, 68, 9, 48, 11, 238, 200, 238, 127, 249, 164, 39, + 134, 106, 134, 210, 139, 213, 242, 126, 92, 104, 242, 184, 52, 29, 156 + ]), + }, + { + name: '256 32 ikm 16 info empty purpose 64 L', + hash: 'sha256', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + purpose: Buffer.from([]), + L: 64, + okm: Buffer.from([ + 51, 160, 206, 70, 126, 207, 132, 181, 69, 177, 200, 56, 41, 184, 112, 236, 248, + 118, 1, 83, 102, 124, 10, 195, 119, 18, 79, 245, 95, 162, 181, 56, 81, 236, 177, + 189, 206, 96, 201, 81, 175, 224, 226, 29, 145, 43, 200, 85, 39, 114, 11, 109, + 149, 95, 25, 11, 115, 33, 203, 94, 254, 126, 91, 130 + ]), + }, + { + name: '256 32 ikm 32 info empty purpose 64 L', + hash: 'sha256', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ]), + purpose: Buffer.from([]), + L: 64, + okm: Buffer.from([ + 52, 56, 249, 137, 118, 241, 81, 3, 106, 148, 187, 233, 144, 50, 94, 163, 67, + 205, 109, 134, 183, 190, 143, 196, 219, 244, 120, 193, 244, 189, 86, 158, 109, + 109, 248, 231, 200, 58, 81, 17, 125, 98, 241, 66, 160, 75, 198, 179, 152, 75, + 78, 124, 238, 111, 9, 61, 193, 16, 48, 103, 202, 38, 129, 54 + ]), + }, + { + name: '256 32 ikm 16 info some purpose 64 L', + hash: 'sha256', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + purpose: Buffer.from([0, 0, 0]), + L: 64, + okm: Buffer.from([ + 149, 140, 56, 1, 42, 245, 141, 18, 147, 234, 181, 53, 117, 134, 205, 36, 207, + 162, 134, 79, 181, 46, 106, 91, 151, 77, 66, 248, 56, 48, 162, 103, 1, 93, 196, + 85, 14, 201, 194, 108, 225, 190, 80, 221, 10, 156, 15, 109, 79, 39, 93, 240, + 17, 198, 82, 18, 26, 230, 140, 175, 200, 70, 243, 73 + ]), + }, + { + name: '256 32 ikm 32 info some purpose 64 L', + hash: 'sha256', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ]), + purpose: Buffer.from([0, 0, 0]), + L: 64, + okm: Buffer.from([ + 246, 171, 10, 194, 229, 245, 91, 201, 41, 183, 21, 176, 40, 248, 85, 39, 158, + 233, 162, 152, 83, 192, 172, 239, 30, 97, 78, 140, 54, 168, 77, 243, 120, 159, + 194, 134, 42, 21, 121, 100, 184, 90, 95, 88, 14, 185, 208, 204, 216, 8, 65, 173, + 63, 200, 95, 76, 110, 57, 165, 239, 184, 191, 32, 129 + ]), + }, + { + name: '384 66 ikm 16 info empty purpose 32 L', + hash: 'sha384', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + purpose: Buffer.from([]), + L: 32, + okm: Buffer.from([ + 204, 31, 218, 43, 73, 249, 90, 197, 128, 11, 55, 209, 21, 96, 149, 204, 65, 250, + 190, 210, 192, 112, 42, 120, 150, 31, 38, 171, 15, 85, 45, 253 + ]), + }, -const t4: TestVector = { - name: '384 with empty info/nonce and purpose', - hash: 'sha384', - ikm: Buffer.from([ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - ]), - info: Buffer.from([]), - purpose: Buffer.from([]), - L: 32, - okm: Buffer.from([ - 77, 201, 182, 192, 122, 4, 68, 27, 246, 185, 15, 31, 62, 179, 227, 246, 181, - 129, 114, 189, 185, 1, 61, 118, 67, 14, 77, 219, 40, 6, 123, 205, - ]), -} - -const t5: TestVector = { - name: '384 with empty purpose', - hash: 'sha384', - ikm: Buffer.from([ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - ]), - info: Buffer.from([0, 0, 0]), - purpose: Buffer.from([]), - L: 32, - okm: Buffer.from([ - 169, 94, 115, 125, 197, 199, 196, 79, 166, 30, 97, 233, 173, 170, 235, 242, - 13, 176, 204, 52, 135, 119, 6, 237, 104, 18, 186, 179, 255, 64, 13, 78, - ]), -} - -const t6: TestVector = { - name: '384 with empty info/nonce', - hash: 'sha384', - ikm: Buffer.from([ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - ]), - info: Buffer.from([]), - purpose: Buffer.from([0, 0, 0]), - L: 32, - okm: Buffer.from([ - 169, 94, 115, 125, 197, 199, 196, 79, 166, 30, 97, 233, 173, 170, 235, 242, - 13, 176, 204, 52, 135, 119, 6, 237, 104, 18, 186, 179, 255, 64, 13, 78, - ]), -} + { + name: '384 66 ikm 32 info empty purpose 32 L', + hash: 'sha384', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ]), + purpose: Buffer.from([]), + L: 32, + okm: Buffer.from([ + 145, 220, 193, 19, 71, 197, 30, 204, 224, 63, 133, 97, 42, 121, 70, 123, 160, + 27, 123, 192, 190, 184, 13, 199, 127, 88, 162, 198, 152, 155, 202, 62 + ]), + }, + { + name: '384 66 ikm 16 info some purpose 32 L', + hash: 'sha384', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + purpose: Buffer.from([0, 0, 0]), + L: 32, + okm: Buffer.from([ + 108, 227, 141, 225, 189, 36, 224, 42, 196, 111, 87, 108, 219, 47, 90, 54, 182, + 250, 91, 139, 155, 32, 44, 63, 224, 169, 27, 220, 89, 92, 198, 205 + ]), + }, + { + name: '384 66 ikm 32 info some purpose 32 L', + hash: 'sha384', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ]), + purpose: Buffer.from([0, 0, 0]), + L: 32, + okm: Buffer.from([ + 39, 20, 91, 243, 52, 136, 88, 52, 191, 193, 105, 132, 150, 194, 213, 128, 27, + 212, 3, 35, 108, 50, 86, 137, 186, 61, 14, 15, 50, 249, 237, 218 + ]), + }, + { + name: '384 66 ikm 16 info empty purpose 64 L', + hash: 'sha384', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + purpose: Buffer.from([]), + L: 64, + okm: Buffer.from([ + 111, 207, 165, 86, 103, 173, 136, 120, 48, 252, 135, 205, 83, 250, 1, 79, 142, + 95, 199, 64, 26, 206, 24, 96, 233, 181, 87, 170, 163, 188, 156, 143, 8, 154, + 91, 67, 105, 48, 15, 1, 231, 173, 105, 23, 41, 68, 46, 57, 82, 96, 81, 129, 143, + 163, 153, 143, 18, 153, 183, 105, 90, 18, 100, 213 + ]), + }, + { + name: '384 66 ikm 32 info empty purpose 64 L', + hash: 'sha384', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ]), + purpose: Buffer.from([]), + L: 64, + okm: Buffer.from([ + 236, 191, 183, 130, 155, 68, 57, 171, 78, 231, 92, 244, 117, 21, 240, 212, 82, + 203, 99, 236, 132, 223, 124, 228, 175, 255, 51, 30, 115, 36, 225, 148, 217, 75, + 221, 198, 255, 176, 106, 186, 171, 107, 242, 62, 100, 22, 37, 232, 90, 102, 1, + 114, 185, 237, 241, 8, 76, 132, 55, 93, 115, 68, 164, 129 + ]), + }, + { + name: '384 66 ikm 16 info some purpose 64 L', + hash: 'sha384', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + purpose: Buffer.from([0, 0, 0]), + L: 64, + okm: Buffer.from([ + 189, 20, 161, 16, 60, 27, 20, 143, 166, 50, 26, 69, 192, 43, 37, 50, 129, 229, + 138, 138, 34, 32, 216, 44, 150, 238, 102, 123, 134, 164, 15, 27, 4, 71, 190, + 148, 21, 174, 103, 238, 40, 80, 205, 218, 227, 159, 207, 182, 62, 1, 157, 153, + 81, 86, 0, 11, 52, 106, 145, 54, 38, 205, 193, 114 + ]), + }, + { + name: '384 66 ikm 32 info some purpose 64 L', + hash: 'sha384', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ]), + purpose: Buffer.from([0, 0, 0]), + L: 64, + okm: Buffer.from([ + 213, 189, 233, 32, 79, 28, 27, 234, 6, 242, 105, 186, 173, 11, 222, 183, 209, + 134, 176, 227, 134, 81, 243, 233, 197, 162, 253, 186, 173, 66, 61, 230, 175, + 170, 182, 122, 129, 114, 59, 179, 170, 150, 160, 116, 189, 167, 155, 45, 40, + 43, 77, 76, 250, 216, 177, 237, 203, 127, 53, 124, 82, 116, 231, 153 + ]), + }, + { + name: '384 48 ikm 16 info empty purpose 32 L', + hash: 'sha384', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + purpose: Buffer.from([]), + L: 32, + okm: Buffer.from([ + 204, 31, 218, 43, 73, 249, 90, 197, 128, 11, 55, 209, 21, 96, 149, 204, 65, 250, + 190, 210, 192, 112, 42, 120, 150, 31, 38, 171, 15, 85, 45, 253 + ]), + }, + { + name: '384 48 ikm 32 info empty purpose 32 L', + hash: 'sha384', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ]), + purpose: Buffer.from([]), + L: 32, + okm: Buffer.from([ + 145, 220, 193, 19, 71, 197, 30, 204, 224, 63, 133, 97, 42, 121, 70, 123, 160, + 27, 123, 192, 190, 184, 13, 199, 127, 88, 162, 198, 152, 155, 202, 62 + ]), + }, + { + name: '384 48 ikm 16 info some purpose 32 L', + hash: 'sha384', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + purpose: Buffer.from([0, 0, 0]), + L: 32, + okm: Buffer.from([ + 108, 227, 141, 225, 189, 36, 224, 42, 196, 111, 87, 108, 219, 47, 90, 54, 182, + 250, 91, 139, 155, 32, 44, 63, 224, 169, 27, 220, 89, 92, 198, 205 + ]), + }, + { + name: '384 48 ikm 32 info some purpose 32 L', + hash: 'sha384', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ]), + purpose: Buffer.from([0, 0, 0]), + L: 32, + okm: Buffer.from([ + 39, 20, 91, 243, 52, 136, 88, 52, 191, 193, 105, 132, 150, 194, 213, 128, 27, + 212, 3, 35, 108, 50, 86, 137, 186, 61, 14, 15, 50, 249, 237, 218 + ]), + }, + { + name: '384 48 ikm 16 info empty purpose 64 L', + hash: 'sha384', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + purpose: Buffer.from([]), + L: 64, + okm: Buffer.from([ + 111, 207, 165, 86, 103, 173, 136, 120, 48, 252, 135, 205, 83, 250, 1, 79, 142, + 95, 199, 64, 26, 206, 24, 96, 233, 181, 87, 170, 163, 188, 156, 143, 8, 154, + 91, 67, 105, 48, 15, 1, 231, 173, 105, 23, 41, 68, 46, 57, 82, 96, 81, 129, 143, + 163, 153, 143, 18, 153, 183, 105, 90, 18, 100, 213 + ]), + }, + { + name: '384 48 ikm 32 info empty purpose 64 L', + hash: 'sha384', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ]), + purpose: Buffer.from([]), + L: 64, + okm: Buffer.from([ + 236, 191, 183, 130, 155, 68, 57, 171, 78, 231, 92, 244, 117, 21, 240, 212, 82, + 203, 99, 236, 132, 223, 124, 228, 175, 255, 51, 30, 115, 36, 225, 148, 217, 75, + 221, 198, 255, 176, 106, 186, 171, 107, 242, 62, 100, 22, 37, 232, 90, 102, 1, + 114, 185, 237, 241, 8, 76, 132, 55, 93, 115, 68, 164, 129 + ]), + }, + { + name: '384 48 ikm 16 info some purpose 64 L', + hash: 'sha384', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + purpose: Buffer.from([0, 0, 0]), + L: 64, + okm: Buffer.from([ + 189, 20, 161, 16, 60, 27, 20, 143, 166, 50, 26, 69, 192, 43, 37, 50, 129, 229, + 138, 138, 34, 32, 216, 44, 150, 238, 102, 123, 134, 164, 15, 27, 4, 71, 190, + 148, 21, 174, 103, 238, 40, 80, 205, 218, 227, 159, 207, 182, 62, 1, 157, 153, + 81, 86, 0, 11, 52, 106, 145, 54, 38, 205, 193, 114 + ]), + }, + { + name: '384 48 ikm 32 info some purpose 64 L', + hash: 'sha384', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ]), + purpose: Buffer.from([0, 0, 0]), + L: 64, + okm: Buffer.from([ + 213, 189, 233, 32, 79, 28, 27, 234, 6, 242, 105, 186, 173, 11, 222, 183, 209, + 134, 176, 227, 134, 81, 243, 233, 197, 162, 253, 186, 173, 66, 61, 230, 175, + 170, 182, 122, 129, 114, 59, 179, 170, 150, 160, 116, 189, 167, 155, 45, 40, + 43, 77, 76, 250, 216, 177, 237, 203, 127, 53, 124, 82, 116, 231, 153 + ]), + }, + { + name: '384 32 ikm 16 info empty purpose 32 L', + hash: 'sha384', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + purpose: Buffer.from([]), + L: 32, + okm: Buffer.from([ + 204, 31, 218, 43, 73, 249, 90, 197, 128, 11, 55, 209, 21, 96, 149, 204, 65, 250, + 190, 210, 192, 112, 42, 120, 150, 31, 38, 171, 15, 85, 45, 253 + ]), + }, + { + name: '384 32 ikm 32 info empty purpose 32 L', + hash: 'sha384', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ]), + purpose: Buffer.from([]), + L: 32, + okm: Buffer.from([ + 145, 220, 193, 19, 71, 197, 30, 204, 224, 63, 133, 97, 42, 121, 70, 123, 160, + 27, 123, 192, 190, 184, 13, 199, 127, 88, 162, 198, 152, 155, 202, 62 + ]), + }, + { + name: '384 32 ikm 16 info some purpose 32 L', + hash: 'sha384', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + purpose: Buffer.from([0, 0, 0]), + L: 32, + okm: Buffer.from([ + 108, 227, 141, 225, 189, 36, 224, 42, 196, 111, 87, 108, 219, 47, 90, 54, 182, + 250, 91, 139, 155, 32, 44, 63, 224, 169, 27, 220, 89, 92, 198, 205 + ]), + }, + { + name: '384 32 ikm 32 info some purpose 32 L', + hash: 'sha384', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ]), + purpose: Buffer.from([0, 0, 0]), + L: 32, + okm: Buffer.from([ + 39, 20, 91, 243, 52, 136, 88, 52, 191, 193, 105, 132, 150, 194, 213, 128, 27, + 212, 3, 35, 108, 50, 86, 137, 186, 61, 14, 15, 50, 249, 237, 218 + ]), + }, + { + name: '384 32 ikm 16 info empty purpose 64 L', + hash: 'sha384', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + purpose: Buffer.from([]), + L: 64, + okm: Buffer.from([ + 111, 207, 165, 86, 103, 173, 136, 120, 48, 252, 135, 205, 83, 250, 1, 79, 142, + 95, 199, 64, 26, 206, 24, 96, 233, 181, 87, 170, 163, 188, 156, 143, 8, 154, + 91, 67, 105, 48, 15, 1, 231, 173, 105, 23, 41, 68, 46, 57, 82, 96, 81, 129, 143, + 163, 153, 143, 18, 153, 183, 105, 90, 18, 100, 213 + ]), + }, + { + name: '384 32 ikm 32 info empty purpose 64 L', + hash: 'sha384', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ]), + purpose: Buffer.from([]), + L: 64, + okm: Buffer.from([ + 236, 191, 183, 130, 155, 68, 57, 171, 78, 231, 92, 244, 117, 21, 240, 212, 82, + 203, 99, 236, 132, 223, 124, 228, 175, 255, 51, 30, 115, 36, 225, 148, 217, 75, + 221, 198, 255, 176, 106, 186, 171, 107, 242, 62, 100, 22, 37, 232, 90, 102, 1, + 114, 185, 237, 241, 8, 76, 132, 55, 93, 115, 68, 164, 129 + ]), + }, + { + name: '384 32 ikm 16 info some purpose 64 L', + hash: 'sha384', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + purpose: Buffer.from([0, 0, 0]), + L: 64, + okm: Buffer.from([ + 189, 20, 161, 16, 60, 27, 20, 143, 166, 50, 26, 69, 192, 43, 37, 50, 129, 229, + 138, 138, 34, 32, 216, 44, 150, 238, 102, 123, 134, 164, 15, 27, 4, 71, 190, + 148, 21, 174, 103, 238, 40, 80, 205, 218, 227, 159, 207, 182, 62, 1, 157, 153, + 81, 86, 0, 11, 52, 106, 145, 54, 38, 205, 193, 114 + ]), + }, + { + name: '384 32 ikm 32 info some purpose 64 L', + hash: 'sha384', + ikm: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ]), + info: Buffer.from([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ]), + purpose: Buffer.from([0, 0, 0]), + L: 64, + okm: Buffer.from([ + 213, 189, 233, 32, 79, 28, 27, 234, 6, 242, 105, 186, 173, 11, 222, 183, 209, + 134, 176, 227, 134, 81, 243, 233, 197, 162, 253, 186, 173, 66, 61, 230, 175, + 170, 182, 122, 129, 114, 59, 179, 170, 150, 160, 116, 189, 167, 155, 45, 40, + 43, 77, 76, 250, 216, 177, 237, 203, 127, 53, 124, 82, 116, 231, 153 + ]), + }, +] export const rawTestVectors = [b1, b2, b3, b4, b5, b6, b7, b8, b9, b10] -export const testVectors = [c1, c2, c3, c4, c5, t1, t2, t3, t4, t5, t6] +export const testVectors = [c1, c2, c3, c4, c5, ...permutedVectors] + export const vectorOkmDigest = Buffer.from([ - 183, 87, 35, 163, 83, 138, 133, 68, 69, 201, 220, 32, 102, 155, 232, 241, 122, - 8, 244, 48, 26, 60, 130, 7, 161, 138, 47, 123, 44, 65, 31, 56, + 100, 105, 118, 112, 24, 213, 47, 164, 113, 176, 211, 130, 28, 237, 167, 5, 250, + 213, 40, 209, 195, 24, 247, 227, 48, 49, 159, 28, 32, 61, 178, 103 ]) export const testVectorDigest = () => diff --git a/modules/kdf-ctr-mode-node/test/kdfctr.test.ts b/modules/kdf-ctr-mode-node/test/kdfctr.test.ts index 6a525d410..d935a5af2 100644 --- a/modules/kdf-ctr-mode-node/test/kdfctr.test.ts +++ b/modules/kdf-ctr-mode-node/test/kdfctr.test.ts @@ -10,7 +10,7 @@ import { SupportedDigestAlgorithms, SupportedDerivedKeyLengths, } from '../src/kdfctr' -import { rawTestVectors, testVectors } from './fixtures' +import { rawTestVectors, testVectors, TestVector } from './fixtures' import { createHash } from 'crypto' describe('KDF Ctr Mode', () => { @@ -43,7 +43,85 @@ describe('KDF Ctr Mode', () => { ).to.throw('The nonce must be provided') }) - it('Precondition: the expected length must be 32 bytes', () => { + + + it('Precondition: the ikm must be 32, 48, 66 bytes long', () => { + const invalidIkm = Buffer.alloc(31) + expect(() => + kdfCounterMode({ + digestAlgorithm, + ikm: invalidIkm, + nonce, + purpose, + expectedLength, + }) + ).to.throw(`Unsupported IKM length ${invalidIkm.length}`) + + expect(() => + kdfCounterMode({ + digestAlgorithm, + ikm: Buffer.alloc(32), + nonce, + purpose, + expectedLength, + }) + ).to.not.throw() + + expect(() => + kdfCounterMode({ + digestAlgorithm, + ikm: Buffer.alloc(48), + nonce, + purpose, + expectedLength, + }) + ).to.not.throw() + + expect(() => + kdfCounterMode({ + digestAlgorithm, + ikm: Buffer.alloc(66), + nonce, + purpose, + expectedLength, + }) + ).to.not.throw() + }) + + it('Precondition: the nonce must be 16, 32 bytes long', () => { + const invalidNonce = Buffer.alloc(17) + expect(() => + kdfCounterMode({ + digestAlgorithm, + ikm, + nonce: invalidNonce, + purpose, + expectedLength, + }) + ).to.throw(`Unsupported nonce length ${invalidNonce.length}`) + + expect(() => + kdfCounterMode({ + digestAlgorithm, + ikm, + nonce: Buffer.alloc(16), + purpose, + expectedLength, + }) + ).to.not.throw() + + expect(() => + kdfCounterMode({ + digestAlgorithm, + ikm, + nonce: Buffer.alloc(32), + purpose, + expectedLength, + }) + ).to.not.throw() + }) + + it('Precondition: the expected length must be 32, 64 bytes', () => { const invalidExpectedLength = 31 as SupportedDerivedKeyLengths expect(() => kdfCounterMode({ @@ -64,6 +142,16 @@ describe('KDF Ctr Mode', () => { expectedLength: 32, }) ).to.not.throw() + + expect(() => + kdfCounterMode({ + digestAlgorithm, + ikm, + nonce, + purpose, + expectedLength: 64, + }) + ).to.not.throw() }) it('Precondition: the expected length * 8 must be under the max 32-bit signed integer', () => { @@ -248,17 +336,19 @@ describe('KDF Ctr Mode', () => { }) for (const testVector of testVectors) { - const { name, hash, ikm, info, L, okm, purpose } = testVector - it(name, () => { - const test = kdfCounterMode({ - digestAlgorithm: hash, - ikm, - nonce: info, - purpose, - expectedLength: L, - }) - expect(test).to.deep.equals(okm) - }) + const { name } = testVector + it(name, () => CheckTestVector(testVector)) } }) }) + +function CheckTestVector({ hash, ikm, info, L, okm, purpose }: TestVector) { + const test = kdfCounterMode({ + digestAlgorithm: hash, + ikm, + nonce: info, + purpose, + expectedLength: L, + }) + expect(test).to.deep.equals(okm) +} From 47b8f0d9236bdc864bb1bc9fe9f647a96ffbdea6 Mon Sep 17 00:00:00 2001 From: seebees Date: Tue, 14 Jan 2025 15:55:18 -0800 Subject: [PATCH 20/25] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Corella <39066999+josecorella@users.noreply.github.com> --- .../src/branch_keystore_helpers.ts | 3 +- .../src/dynamodb_key_storage.ts | 9 +++-- .../test/branch_keystore_helpers.test.ts | 3 -- modules/example-node/hkr-demo/hkr.ts | 2 +- .../src/kms_hkeyring_node_helpers.ts | 34 ++----------------- 5 files changed, 10 insertions(+), 41 deletions(-) diff --git a/modules/branch-keystore-node/src/branch_keystore_helpers.ts b/modules/branch-keystore-node/src/branch_keystore_helpers.ts index 25d49c097..8616d92f8 100644 --- a/modules/branch-keystore-node/src/branch_keystore_helpers.ts +++ b/modules/branch-keystore-node/src/branch_keystore_helpers.ts @@ -11,7 +11,6 @@ import { import { unmarshall } from '@aws-sdk/util-dynamodb' import { BranchKeyItem, BranchKeyRecord } from './branch_keystore_structures' import { EncryptedHierarchicalKey, BranchKeyEncryptionContext } from './types' -// import { IBranchKeyStoreNode } from './branch_keystore' import { DecryptCommand } from '@aws-sdk/client-kms' import { KmsKeyConfig } from './kms_config' import { @@ -71,7 +70,7 @@ export async function getBranchKeyItem( // error out if there is not Item field (record not found) needs( responseItem, - `A branch key record with ${PARTITION_KEY}=${partitionValue} and ${SORT_KEY}=${sortValue} was not found in DynamoDB` + `A branch key record with ${PARTITION_KEY}=${partitionValue} and ${SORT_KEY}=${sortValue} was not found in the DynamoDB table ${ddbTableName}.` ) // at this point, we got back a record so convert the DDB response item into // a more JS-friendly object diff --git a/modules/branch-keystore-node/src/dynamodb_key_storage.ts b/modules/branch-keystore-node/src/dynamodb_key_storage.ts index a7e6159a9..59ef55abb 100644 --- a/modules/branch-keystore-node/src/dynamodb_key_storage.ts +++ b/modules/branch-keystore-node/src/dynamodb_key_storage.ts @@ -58,9 +58,9 @@ export class DynamoDBKeyStorage implements IBranchKeyStorage { needs( typeof logicalKeyStoreName === 'string', - 'DDB table name must be a string' + 'Logical Key Store name must be a string' ) - +needs(logicalKeyStoreName, 'Logical Key Store name required') /* Precondition: DDB client must be a DynamoDBClient */ needs( ddbClient instanceof DynamoDBClient, @@ -154,7 +154,10 @@ export class DynamoDBKeyStorage implements IBranchKeyStorage { //= aws-encryption-sdk-specification/framework/key-store/dynamodb-key-storage.md#getencryptedbranchkeyversion //# The returned EncryptedHierarchicalKey MUST have the same identifier as the input. - needs(encrypted.branchKeyId == branchKeyId, 'Unexpected branch key id.') + needs( + encrypted.branchKeyId == branchKeyId, + 'Unexpected branch key id. Expected ${branchKeyId}, found ${encrypted.branchKeyId}' + ) //= aws-encryption-sdk-specification/framework/key-store/dynamodb-key-storage.md#getencryptedbranchkeyversion //# The returned EncryptedHierarchicalKey MUST have the same version as the input. diff --git a/modules/branch-keystore-node/test/branch_keystore_helpers.test.ts b/modules/branch-keystore-node/test/branch_keystore_helpers.test.ts index b8d42bb4b..ae5ab84c8 100644 --- a/modules/branch-keystore-node/test/branch_keystore_helpers.test.ts +++ b/modules/branch-keystore-node/test/branch_keystore_helpers.test.ts @@ -42,9 +42,6 @@ import { import { DynamoDBKeyStorage } from '../src/dynamodb_key_storage' import { EncryptedHierarchicalKey, - // ActiveKeyEncryptionContext, - // VersionKeyEncryptionContext, - // BranchKeyVersionType, } from '../src/types' const VALID_CUSTOM_ENCRYPTION_CONTEXT_KV_PAIRS = { diff --git a/modules/example-node/hkr-demo/hkr.ts b/modules/example-node/hkr-demo/hkr.ts index 22c752336..4b71821a3 100644 --- a/modules/example-node/hkr-demo/hkr.ts +++ b/modules/example-node/hkr-demo/hkr.ts @@ -18,7 +18,7 @@ const { encrypt, decrypt } = buildClient( const MAX_INPUT_LENGTH = 20 const MIN_INPUT_LENGTH = 15 const PURPLE_LOG = '\x1b[35m%s\x1b[0m' -const YELLO_LOG = '\x1b[33m%s\x1b[0m' +const YELLOW_LOG = '\x1b[33m%s\x1b[0m' const GREEN_LOG = '\x1b[32m%s\x1b[0m' const RED_LOG = '\x1b[31m%s\x1b[0m' diff --git a/modules/kms-keyring-node/src/kms_hkeyring_node_helpers.ts b/modules/kms-keyring-node/src/kms_hkeyring_node_helpers.ts index bdc42f0c3..2ecb10c2d 100644 --- a/modules/kms-keyring-node/src/kms_hkeyring_node_helpers.ts +++ b/modules/kms-keyring-node/src/kms_hkeyring_node_helpers.ts @@ -19,13 +19,10 @@ import { createHash, randomBytes, } from 'crypto' -// import { uInt32BE } from '@aws-crypto/serialize' import { CryptographicMaterialsCache } from '@aws-crypto/cache-material' import { kdfCounterMode } from '@aws-crypto/kdf-ctr-mode-node' import { - // ACTIVE_AS_BYTES, CACHE_ENTRY_ID_DIGEST_ALGORITHM, - // CACHE_ENTRY_ID_LENGTH, CIPHERTEXT_STRUCTURE, DECRYPT_FLAGS, DERIVED_BRANCH_KEY_LENGTH, @@ -50,8 +47,6 @@ export const { uuidv4ToCompressedBytes, decompressBytesToUuidv4 } = uuidv4Factory(stringToHexBytes, hexBytesToString) export const { serializeEncryptionContext } = serializeFactory(stringToUtf8Bytes) -// const stringToAsciiBytes = (input: string): Buffer => -// Buffer.from(input, 'ascii') export function getBranchKeyId( { branchKeyId, branchKeyIdSupplier }: IKmsHierarchicalKeyRingNode, @@ -185,33 +180,8 @@ export function getCacheEntryId( ]) } - // const entryInfo = versionAsBytes - // ? Buffer.concat([ - // RESOURCE_ID, - // NULL_BYTE, - // DECRYPTION_SCOPE, - // NULL_BYTE, - // partitionId, - // NULL_BYTE, - // logicalKeyStoreName, - // NULL_BYTE, - // branchKeyIdAsBytes, - // NULL_BYTE, - // versionAsBytes, - // ]) - // : Buffer.concat([ - // RESOURCE_ID, - // NULL_BYTE, - // ENCRYPTION_SCOPE, - // NULL_BYTE, - // partitionId, - // NULL_BYTE, - // logicalKeyStoreName, - // NULL_BYTE, - // branchKeyIdAsBytes, - // ]) - - // encrypt the branch key id buffer with sha512 + + // hash the branch key id buffer with sha512 return createHash(CACHE_ENTRY_ID_DIGEST_ALGORITHM) .update(entryInfo) .digest() From ac71de30011bd38a2982a5d1597f50f1e9c05b0d Mon Sep 17 00:00:00 2001 From: seebees Date: Tue, 14 Jan 2025 16:05:15 -0800 Subject: [PATCH 21/25] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Corella <39066999+josecorella@users.noreply.github.com> Co-authored-by: Tony Knapp <5892063+texastony@users.noreply.github.com> --- modules/branch-keystore-node/src/branch_keystore.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/branch-keystore-node/src/branch_keystore.ts b/modules/branch-keystore-node/src/branch_keystore.ts index 13b8822f0..f653b4006 100644 --- a/modules/branch-keystore-node/src/branch_keystore.ts +++ b/modules/branch-keystore-node/src/branch_keystore.ts @@ -274,7 +274,7 @@ export class BranchKeyStoreNode implements IBranchKeyStoreNode { needs( activeEncryptedBranchKey.type instanceof ActiveHierarchicalSymmetricVersion, - 'Unexpected type. Not an version record.' + 'Unexpected type. Not a version record.' ) //= aws-encryption-sdk-specification/framework/branch-key-store.md#getactivebranchkey @@ -282,7 +282,7 @@ export class BranchKeyStoreNode implements IBranchKeyStoreNode { needs( activeEncryptedBranchKey.encryptionContext[TABLE_FIELD] == this.logicalKeyStoreName, - 'Unexpected logical table name.' + 'Unexpected logical table name. Expected ${this.logicalKeyStoreName}, found ${activeEncryptedBranchKey.encryptionContext[TABLE_FIELD]}.' ) //= aws-encryption-sdk-specification/framework/branch-key-store.md#getactivebranchkey @@ -319,7 +319,7 @@ export class BranchKeyStoreNode implements IBranchKeyStoreNode { 'MUST supply a string branch key id' ) needs( - branchKeyId && branchKeyVersion, + branchKeyVersion && typeof branchKeyVersion === 'string, 'MUST supply a string branch key version' ) @@ -352,7 +352,7 @@ export class BranchKeyStoreNode implements IBranchKeyStoreNode { //# GetActiveBranchKey MUST verify that the returned EncryptedHierarchicalKey is an HierarchicalSymmetricVersion. needs( encryptedBranchKey.type instanceof HierarchicalSymmetricVersion, - 'Unexpected type. Not an version record.' + 'Unexpected type. Not a version record.' ) //= aws-encryption-sdk-specification/framework/branch-key-store.md#getbranchkeyversion @@ -360,7 +360,7 @@ export class BranchKeyStoreNode implements IBranchKeyStoreNode { needs( encryptedBranchKey.encryptionContext[TABLE_FIELD] == this.logicalKeyStoreName, - 'Unexpected logical table name.' + 'Unexpected logical table name. Expected ${this.logicalKeyStoreName}, found ${encryptedBranchKey.encryptionContext[TABLE_FIELD}.' ) //= aws-encryption-sdk-specification/framework/branch-key-store.md#getbranchkeyversion From dc448dd81cbad2f6495eba91ecd1b115bb4d8804 Mon Sep 17 00:00:00 2001 From: seebees Date: Tue, 14 Jan 2025 16:02:35 -0800 Subject: [PATCH 22/25] update readme --- modules/kdf-ctr-mode-node/README.md | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/modules/kdf-ctr-mode-node/README.md b/modules/kdf-ctr-mode-node/README.md index 6b89b9302..7225115ff 100644 --- a/modules/kdf-ctr-mode-node/README.md +++ b/modules/kdf-ctr-mode-node/README.md @@ -17,10 +17,21 @@ npm install @aws-crypto/kdf-ctr-mode-node ## use ```javascript -const HKDF = require('@aws-crypto/hkdf-node') -const expand = HKDF('sha256')('some key', 'some salt') -const info = { some: 'info', message_id: 123 } -const key = expand(32, Buffer.from(JSON.stringify(info))) + +const digestAlgorithm = 'sha256' +const initialKeyMaterial = gottenFromSomewhereSecure() +const nonce = freshRandomData() +const purpose = Buffer.from('What this derived key is for.', 'utf-8') +const expectedLength = 32 + +const KDF = require('@aws-crypto/kdf-ctr-mode-node') +const derivedKey = KDF.kdfCounterMode({ + digestAlgorithm, + ikm: initialKeyMaterial, + nonce, + purpose, + expectedLength, + }) ``` ## test From c7d39687e041be5b19dee59c8e6a8fd83bb814ae Mon Sep 17 00:00:00 2001 From: seebees Date: Tue, 14 Jan 2025 16:13:38 -0800 Subject: [PATCH 23/25] lint and tests --- .../src/branch_keystore.ts | 2 +- .../src/dynamodb_key_storage.ts | 8 +- .../test/branch_keystore.test.ts | 4 +- .../test/branch_keystore_helpers.test.ts | 10 +- modules/kdf-ctr-mode-node/src/kdfctr.ts | 1 - modules/kdf-ctr-mode-node/test/fixtures.ts | 311 +++++++++--------- modules/kdf-ctr-mode-node/test/kdfctr.test.ts | 2 - .../src/kms_hkeyring_node_helpers.ts | 1 - 8 files changed, 175 insertions(+), 164 deletions(-) diff --git a/modules/branch-keystore-node/src/branch_keystore.ts b/modules/branch-keystore-node/src/branch_keystore.ts index f653b4006..8dcd37975 100644 --- a/modules/branch-keystore-node/src/branch_keystore.ts +++ b/modules/branch-keystore-node/src/branch_keystore.ts @@ -319,7 +319,7 @@ export class BranchKeyStoreNode implements IBranchKeyStoreNode { 'MUST supply a string branch key id' ) needs( - branchKeyVersion && typeof branchKeyVersion === 'string, + branchKeyVersion && typeof branchKeyVersion === 'string', 'MUST supply a string branch key version' ) diff --git a/modules/branch-keystore-node/src/dynamodb_key_storage.ts b/modules/branch-keystore-node/src/dynamodb_key_storage.ts index 59ef55abb..7e34620f5 100644 --- a/modules/branch-keystore-node/src/dynamodb_key_storage.ts +++ b/modules/branch-keystore-node/src/dynamodb_key_storage.ts @@ -60,7 +60,7 @@ export class DynamoDBKeyStorage implements IBranchKeyStorage { typeof logicalKeyStoreName === 'string', 'Logical Key Store name must be a string' ) -needs(logicalKeyStoreName, 'Logical Key Store name required') + needs(logicalKeyStoreName, 'Logical Key Store name required') /* Precondition: DDB client must be a DynamoDBClient */ needs( ddbClient instanceof DynamoDBClient, @@ -155,9 +155,9 @@ needs(logicalKeyStoreName, 'Logical Key Store name required') //= aws-encryption-sdk-specification/framework/key-store/dynamodb-key-storage.md#getencryptedbranchkeyversion //# The returned EncryptedHierarchicalKey MUST have the same identifier as the input. needs( - encrypted.branchKeyId == branchKeyId, - 'Unexpected branch key id. Expected ${branchKeyId}, found ${encrypted.branchKeyId}' - ) + encrypted.branchKeyId == branchKeyId, + 'Unexpected branch key id. Expected ${branchKeyId}, found ${encrypted.branchKeyId}' + ) //= aws-encryption-sdk-specification/framework/key-store/dynamodb-key-storage.md#getencryptedbranchkeyversion //# The returned EncryptedHierarchicalKey MUST have the same version as the input. diff --git a/modules/branch-keystore-node/test/branch_keystore.test.ts b/modules/branch-keystore-node/test/branch_keystore.test.ts index ba59b2ff5..d581d8e3a 100644 --- a/modules/branch-keystore-node/test/branch_keystore.test.ts +++ b/modules/branch-keystore-node/test/branch_keystore.test.ts @@ -480,7 +480,7 @@ describe('Test Branch keystore', () => { logicalKeyStoreName: '', kmsConfiguration: KMS_CONFIGURATION, }) - ).to.throw('Logical Keystore name required') + ).to.throw('Logical Key Store name required') }) describe('Test proper init', () => { @@ -696,7 +696,7 @@ describe('Test Branch keystore', () => { void (await expect( keyStore.getActiveBranchKey('Robbie') ).to.be.rejectedWith( - `A branch key record with ${PARTITION_KEY}=Robbie and ${SORT_KEY}=${BRANCH_KEY_ACTIVE_TYPE} was not found in DynamoDB` + `A branch key record with ${PARTITION_KEY}=Robbie and ${SORT_KEY}=${BRANCH_KEY_ACTIVE_TYPE} was not found in the DynamoDB table ${DDB_TABLE_NAME}.` )) }) diff --git a/modules/branch-keystore-node/test/branch_keystore_helpers.test.ts b/modules/branch-keystore-node/test/branch_keystore_helpers.test.ts index ae5ab84c8..cb510079a 100644 --- a/modules/branch-keystore-node/test/branch_keystore_helpers.test.ts +++ b/modules/branch-keystore-node/test/branch_keystore_helpers.test.ts @@ -40,9 +40,7 @@ import { SORT_KEY, } from '../src/constants' import { DynamoDBKeyStorage } from '../src/dynamodb_key_storage' -import { - EncryptedHierarchicalKey, -} from '../src/types' +import { EncryptedHierarchicalKey } from '../src/types' const VALID_CUSTOM_ENCRYPTION_CONTEXT_KV_PAIRS = { 'aws-crypto-ec:key1': 'value 1', @@ -116,7 +114,7 @@ describe('Test keystore helpers', () => { BRANCH_KEY_ACTIVE_TYPE ) ).to.rejectedWith( - `A branch key record with ${PARTITION_KEY}=${nonexistentBranchKeyId} and ${SORT_KEY}=${BRANCH_KEY_ACTIVE_TYPE} was not found in DynamoDB` + `A branch key record with ${PARTITION_KEY}=${nonexistentBranchKeyId} and ${SORT_KEY}=${BRANCH_KEY_ACTIVE_TYPE} was not found in the DynamoDB table ${DDB_TABLE_NAME}.` )) void (await expect( @@ -126,7 +124,7 @@ describe('Test keystore helpers', () => { VERSION_BRANCH_KEY[TYPE_FIELD] ) ).to.be.rejectedWith( - `A branch key record with ${PARTITION_KEY}=${nonexistentBranchKeyId} and ${SORT_KEY}=${VERSION_BRANCH_KEY[TYPE_FIELD]} was not found in DynamoDB` + `A branch key record with ${PARTITION_KEY}=${nonexistentBranchKeyId} and ${SORT_KEY}=${VERSION_BRANCH_KEY[TYPE_FIELD]} was not found in the DynamoDB table ${DDB_TABLE_NAME}.` )) }) @@ -137,7 +135,7 @@ describe('Test keystore helpers', () => { void (await expect( getBranchKeyItem(BRANCH_KEY_STORAGE, BRANCH_KEY_ID, nonexistentType) ).to.be.rejectedWith( - `A branch key record with ${PARTITION_KEY}=${BRANCH_KEY_ID} and ${SORT_KEY}=${nonexistentType} was not found in DynamoDB` + `A branch key record with ${PARTITION_KEY}=${BRANCH_KEY_ID} and ${SORT_KEY}=${nonexistentType} was not found in the DynamoDB table ${DDB_TABLE_NAME}.` )) }) }) diff --git a/modules/kdf-ctr-mode-node/src/kdfctr.ts b/modules/kdf-ctr-mode-node/src/kdfctr.ts index 58835d459..bc9718b3c 100644 --- a/modules/kdf-ctr-mode-node/src/kdfctr.ts +++ b/modules/kdf-ctr-mode-node/src/kdfctr.ts @@ -37,7 +37,6 @@ export function kdfCounterMode({ purpose, expectedLength, }: KdfCtrInput): Buffer { - /* Precondition: the ikm must be 32, 48, 66 bytes long */ needs( SUPPORTED_IKM_LENGTHS.includes(ikm.length), diff --git a/modules/kdf-ctr-mode-node/test/fixtures.ts b/modules/kdf-ctr-mode-node/test/fixtures.ts index a5bb6b5b8..93371f578 100644 --- a/modules/kdf-ctr-mode-node/test/fixtures.ts +++ b/modules/kdf-ctr-mode-node/test/fixtures.ts @@ -348,8 +348,9 @@ const permutedVectors: TestVector[] = [ purpose: Buffer.from([]), L: 32, okm: Buffer.from([ - 65, 81, 193, 63, 209, 164, 150, 11, 109, 207, 92, 45, 90, 68, 135, 42, 123, 180, - 253, 81, 14, 247, 137, 101, 193, 167, 240, 151, 189, 236, 116, 145 + 65, 81, 193, 63, 209, 164, 150, 11, 109, 207, 92, 45, 90, 68, 135, 42, + 123, 180, 253, 81, 14, 247, 137, 101, 193, 167, 240, 151, 189, 236, 116, + 145, ]), }, { @@ -367,8 +368,8 @@ const permutedVectors: TestVector[] = [ purpose: Buffer.from([]), L: 32, okm: Buffer.from([ - 63, 229, 35, 81, 172, 145, 120, 135, 156, 61, 161, 195, 145, 204, 129, 113, 88, - 159, 215, 249, 28, 99, 136, 50, 228, 209, 246, 7, 231, 2, 57, 5 + 63, 229, 35, 81, 172, 145, 120, 135, 156, 61, 161, 195, 145, 204, 129, + 113, 88, 159, 215, 249, 28, 99, 136, 50, 228, 209, 246, 7, 231, 2, 57, 5, ]), }, { @@ -383,8 +384,8 @@ const permutedVectors: TestVector[] = [ purpose: Buffer.from([0, 0, 0]), L: 32, okm: Buffer.from([ - 98, 2, 240, 78, 90, 169, 82, 88, 245, 16, 130, 24, 0, 235, 127, 119, 7, 81, 31, - 246, 185, 49, 55, 210, 90, 80, 24, 70, 110, 41, 80, 231 + 98, 2, 240, 78, 90, 169, 82, 88, 245, 16, 130, 24, 0, 235, 127, 119, 7, + 81, 31, 246, 185, 49, 55, 210, 90, 80, 24, 70, 110, 41, 80, 231, ]), }, { @@ -402,8 +403,8 @@ const permutedVectors: TestVector[] = [ purpose: Buffer.from([0, 0, 0]), L: 32, okm: Buffer.from([ - 97, 53, 70, 12, 112, 51, 12, 163, 48, 38, 248, 168, 7, 126, 186, 238, 152, 50, - 40, 209, 180, 179, 172, 51, 36, 67, 137, 82, 243, 93, 201, 20 + 97, 53, 70, 12, 112, 51, 12, 163, 48, 38, 248, 168, 7, 126, 186, 238, 152, + 50, 40, 209, 180, 179, 172, 51, 36, 67, 137, 82, 243, 93, 201, 20, ]), }, { @@ -418,10 +419,10 @@ const permutedVectors: TestVector[] = [ purpose: Buffer.from([]), L: 64, okm: Buffer.from([ - 29, 73, 171, 201, 120, 179, 112, 19, 246, 140, 115, 142, 46, 127, 229, 46, 72, - 181, 250, 14, 146, 240, 162, 52, 225, 209, 224, 101, 40, 144, 174, 233, 202, - 251, 94, 49, 78, 238, 46, 141, 66, 87, 39, 122, 48, 67, 11, 43, 132, 139, 127, - 9, 146, 233, 15, 96, 169, 161, 238, 106, 15, 98, 250, 100 + 29, 73, 171, 201, 120, 179, 112, 19, 246, 140, 115, 142, 46, 127, 229, 46, + 72, 181, 250, 14, 146, 240, 162, 52, 225, 209, 224, 101, 40, 144, 174, + 233, 202, 251, 94, 49, 78, 238, 46, 141, 66, 87, 39, 122, 48, 67, 11, 43, + 132, 139, 127, 9, 146, 233, 15, 96, 169, 161, 238, 106, 15, 98, 250, 100, ]), }, { @@ -439,10 +440,11 @@ const permutedVectors: TestVector[] = [ purpose: Buffer.from([]), L: 64, okm: Buffer.from([ - 168, 235, 151, 41, 93, 127, 145, 9, 55, 72, 50, 39, 48, 9, 25, 117, 113, 153, - 194, 226, 105, 208, 44, 28, 23, 144, 20, 196, 52, 50, 174, 65, 250, 186, 247, - 162, 147, 155, 66, 63, 129, 179, 206, 216, 220, 160, 58, 52, 114, 87, 169, 239, - 244, 163, 51, 247, 41, 210, 158, 216, 128, 70, 222, 230 + 168, 235, 151, 41, 93, 127, 145, 9, 55, 72, 50, 39, 48, 9, 25, 117, 113, + 153, 194, 226, 105, 208, 44, 28, 23, 144, 20, 196, 52, 50, 174, 65, 250, + 186, 247, 162, 147, 155, 66, 63, 129, 179, 206, 216, 220, 160, 58, 52, + 114, 87, 169, 239, 244, 163, 51, 247, 41, 210, 158, 216, 128, 70, 222, + 230, ]), }, { @@ -457,10 +459,10 @@ const permutedVectors: TestVector[] = [ purpose: Buffer.from([0, 0, 0]), L: 64, okm: Buffer.from([ - 12, 94, 33, 17, 175, 237, 91, 177, 171, 116, 128, 189, 58, 60, 35, 70, 7, 12, - 53, 47, 217, 145, 25, 179, 69, 54, 162, 177, 226, 48, 137, 2, 72, 233, 22, 20, - 25, 82, 234, 247, 45, 14, 65, 80, 16, 14, 133, 64, 128, 253, 210, 84, 181, 13, - 68, 110, 118, 45, 105, 247, 120, 4, 57, 97 + 12, 94, 33, 17, 175, 237, 91, 177, 171, 116, 128, 189, 58, 60, 35, 70, 7, + 12, 53, 47, 217, 145, 25, 179, 69, 54, 162, 177, 226, 48, 137, 2, 72, 233, + 22, 20, 25, 82, 234, 247, 45, 14, 65, 80, 16, 14, 133, 64, 128, 253, 210, + 84, 181, 13, 68, 110, 118, 45, 105, 247, 120, 4, 57, 97, ]), }, { @@ -478,10 +480,11 @@ const permutedVectors: TestVector[] = [ purpose: Buffer.from([0, 0, 0]), L: 64, okm: Buffer.from([ - 133, 88, 230, 171, 118, 242, 195, 174, 144, 189, 243, 255, 178, 76, 43, 19, 194, - 230, 127, 121, 105, 112, 149, 112, 136, 24, 142, 152, 72, 52, 50, 69, 143, 28, - 169, 139, 172, 17, 203, 177, 172, 97, 93, 185, 90, 95, 2, 194, 204, 207, 200, - 246, 134, 217, 205, 160, 114, 77, 166, 84, 132, 196, 215, 127 + 133, 88, 230, 171, 118, 242, 195, 174, 144, 189, 243, 255, 178, 76, 43, + 19, 194, 230, 127, 121, 105, 112, 149, 112, 136, 24, 142, 152, 72, 52, 50, + 69, 143, 28, 169, 139, 172, 17, 203, 177, 172, 97, 93, 185, 90, 95, 2, + 194, 204, 207, 200, 246, 134, 217, 205, 160, 114, 77, 166, 84, 132, 196, + 215, 127, ]), }, { @@ -495,8 +498,8 @@ const permutedVectors: TestVector[] = [ purpose: Buffer.from([]), L: 32, okm: Buffer.from([ - 174, 225, 122, 80, 241, 5, 205, 225, 176, 135, 15, 182, 95, 87, 111, 246, 179, - 190, 55, 74, 187, 21, 81, 32, 218, 198, 229, 39, 214, 223, 52, 110 + 174, 225, 122, 80, 241, 5, 205, 225, 176, 135, 15, 182, 95, 87, 111, 246, + 179, 190, 55, 74, 187, 21, 81, 32, 218, 198, 229, 39, 214, 223, 52, 110, ]), }, { @@ -513,8 +516,8 @@ const permutedVectors: TestVector[] = [ purpose: Buffer.from([]), L: 32, okm: Buffer.from([ - 120, 20, 205, 190, 36, 210, 128, 96, 114, 37, 231, 140, 178, 104, 29, 88, 17, - 78, 64, 239, 106, 201, 187, 236, 162, 228, 238, 246, 2, 253, 21, 98 + 120, 20, 205, 190, 36, 210, 128, 96, 114, 37, 231, 140, 178, 104, 29, 88, + 17, 78, 64, 239, 106, 201, 187, 236, 162, 228, 238, 246, 2, 253, 21, 98, ]), }, { @@ -528,8 +531,9 @@ const permutedVectors: TestVector[] = [ purpose: Buffer.from([0, 0, 0]), L: 32, okm: Buffer.from([ - 50, 227, 144, 224, 232, 121, 50, 114, 63, 144, 192, 211, 168, 146, 64, 210, 94, - 59, 133, 30, 236, 141, 125, 28, 54, 110, 201, 185, 213, 40, 164, 253 + 50, 227, 144, 224, 232, 121, 50, 114, 63, 144, 192, 211, 168, 146, 64, + 210, 94, 59, 133, 30, 236, 141, 125, 28, 54, 110, 201, 185, 213, 40, 164, + 253, ]), }, { @@ -546,8 +550,9 @@ const permutedVectors: TestVector[] = [ purpose: Buffer.from([0, 0, 0]), L: 32, okm: Buffer.from([ - 232, 223, 75, 187, 68, 128, 68, 9, 48, 11, 238, 200, 238, 127, 249, 164, 39, - 134, 106, 134, 210, 139, 213, 242, 126, 92, 104, 242, 184, 52, 29, 156 + 232, 223, 75, 187, 68, 128, 68, 9, 48, 11, 238, 200, 238, 127, 249, 164, + 39, 134, 106, 134, 210, 139, 213, 242, 126, 92, 104, 242, 184, 52, 29, + 156, ]), }, { @@ -561,10 +566,10 @@ const permutedVectors: TestVector[] = [ purpose: Buffer.from([]), L: 64, okm: Buffer.from([ - 51, 160, 206, 70, 126, 207, 132, 181, 69, 177, 200, 56, 41, 184, 112, 236, 248, - 118, 1, 83, 102, 124, 10, 195, 119, 18, 79, 245, 95, 162, 181, 56, 81, 236, 177, - 189, 206, 96, 201, 81, 175, 224, 226, 29, 145, 43, 200, 85, 39, 114, 11, 109, - 149, 95, 25, 11, 115, 33, 203, 94, 254, 126, 91, 130 + 51, 160, 206, 70, 126, 207, 132, 181, 69, 177, 200, 56, 41, 184, 112, 236, + 248, 118, 1, 83, 102, 124, 10, 195, 119, 18, 79, 245, 95, 162, 181, 56, + 81, 236, 177, 189, 206, 96, 201, 81, 175, 224, 226, 29, 145, 43, 200, 85, + 39, 114, 11, 109, 149, 95, 25, 11, 115, 33, 203, 94, 254, 126, 91, 130, ]), }, { @@ -581,10 +586,11 @@ const permutedVectors: TestVector[] = [ purpose: Buffer.from([]), L: 64, okm: Buffer.from([ - 52, 56, 249, 137, 118, 241, 81, 3, 106, 148, 187, 233, 144, 50, 94, 163, 67, - 205, 109, 134, 183, 190, 143, 196, 219, 244, 120, 193, 244, 189, 86, 158, 109, - 109, 248, 231, 200, 58, 81, 17, 125, 98, 241, 66, 160, 75, 198, 179, 152, 75, - 78, 124, 238, 111, 9, 61, 193, 16, 48, 103, 202, 38, 129, 54 + 52, 56, 249, 137, 118, 241, 81, 3, 106, 148, 187, 233, 144, 50, 94, 163, + 67, 205, 109, 134, 183, 190, 143, 196, 219, 244, 120, 193, 244, 189, 86, + 158, 109, 109, 248, 231, 200, 58, 81, 17, 125, 98, 241, 66, 160, 75, 198, + 179, 152, 75, 78, 124, 238, 111, 9, 61, 193, 16, 48, 103, 202, 38, 129, + 54, ]), }, { @@ -598,10 +604,10 @@ const permutedVectors: TestVector[] = [ purpose: Buffer.from([0, 0, 0]), L: 64, okm: Buffer.from([ - 149, 140, 56, 1, 42, 245, 141, 18, 147, 234, 181, 53, 117, 134, 205, 36, 207, - 162, 134, 79, 181, 46, 106, 91, 151, 77, 66, 248, 56, 48, 162, 103, 1, 93, 196, - 85, 14, 201, 194, 108, 225, 190, 80, 221, 10, 156, 15, 109, 79, 39, 93, 240, - 17, 198, 82, 18, 26, 230, 140, 175, 200, 70, 243, 73 + 149, 140, 56, 1, 42, 245, 141, 18, 147, 234, 181, 53, 117, 134, 205, 36, + 207, 162, 134, 79, 181, 46, 106, 91, 151, 77, 66, 248, 56, 48, 162, 103, + 1, 93, 196, 85, 14, 201, 194, 108, 225, 190, 80, 221, 10, 156, 15, 109, + 79, 39, 93, 240, 17, 198, 82, 18, 26, 230, 140, 175, 200, 70, 243, 73, ]), }, { @@ -618,10 +624,10 @@ const permutedVectors: TestVector[] = [ purpose: Buffer.from([0, 0, 0]), L: 64, okm: Buffer.from([ - 246, 171, 10, 194, 229, 245, 91, 201, 41, 183, 21, 176, 40, 248, 85, 39, 158, - 233, 162, 152, 83, 192, 172, 239, 30, 97, 78, 140, 54, 168, 77, 243, 120, 159, - 194, 134, 42, 21, 121, 100, 184, 90, 95, 88, 14, 185, 208, 204, 216, 8, 65, 173, - 63, 200, 95, 76, 110, 57, 165, 239, 184, 191, 32, 129 + 246, 171, 10, 194, 229, 245, 91, 201, 41, 183, 21, 176, 40, 248, 85, 39, + 158, 233, 162, 152, 83, 192, 172, 239, 30, 97, 78, 140, 54, 168, 77, 243, + 120, 159, 194, 134, 42, 21, 121, 100, 184, 90, 95, 88, 14, 185, 208, 204, + 216, 8, 65, 173, 63, 200, 95, 76, 110, 57, 165, 239, 184, 191, 32, 129, ]), }, { @@ -635,8 +641,8 @@ const permutedVectors: TestVector[] = [ purpose: Buffer.from([]), L: 32, okm: Buffer.from([ - 174, 225, 122, 80, 241, 5, 205, 225, 176, 135, 15, 182, 95, 87, 111, 246, 179, - 190, 55, 74, 187, 21, 81, 32, 218, 198, 229, 39, 214, 223, 52, 110 + 174, 225, 122, 80, 241, 5, 205, 225, 176, 135, 15, 182, 95, 87, 111, 246, + 179, 190, 55, 74, 187, 21, 81, 32, 218, 198, 229, 39, 214, 223, 52, 110, ]), }, { @@ -653,8 +659,8 @@ const permutedVectors: TestVector[] = [ purpose: Buffer.from([]), L: 32, okm: Buffer.from([ - 120, 20, 205, 190, 36, 210, 128, 96, 114, 37, 231, 140, 178, 104, 29, 88, 17, - 78, 64, 239, 106, 201, 187, 236, 162, 228, 238, 246, 2, 253, 21, 98 + 120, 20, 205, 190, 36, 210, 128, 96, 114, 37, 231, 140, 178, 104, 29, 88, + 17, 78, 64, 239, 106, 201, 187, 236, 162, 228, 238, 246, 2, 253, 21, 98, ]), }, { @@ -668,8 +674,9 @@ const permutedVectors: TestVector[] = [ purpose: Buffer.from([0, 0, 0]), L: 32, okm: Buffer.from([ - 50, 227, 144, 224, 232, 121, 50, 114, 63, 144, 192, 211, 168, 146, 64, 210, 94, - 59, 133, 30, 236, 141, 125, 28, 54, 110, 201, 185, 213, 40, 164, 253 + 50, 227, 144, 224, 232, 121, 50, 114, 63, 144, 192, 211, 168, 146, 64, + 210, 94, 59, 133, 30, 236, 141, 125, 28, 54, 110, 201, 185, 213, 40, 164, + 253, ]), }, { @@ -686,8 +693,9 @@ const permutedVectors: TestVector[] = [ purpose: Buffer.from([0, 0, 0]), L: 32, okm: Buffer.from([ - 232, 223, 75, 187, 68, 128, 68, 9, 48, 11, 238, 200, 238, 127, 249, 164, 39, - 134, 106, 134, 210, 139, 213, 242, 126, 92, 104, 242, 184, 52, 29, 156 + 232, 223, 75, 187, 68, 128, 68, 9, 48, 11, 238, 200, 238, 127, 249, 164, + 39, 134, 106, 134, 210, 139, 213, 242, 126, 92, 104, 242, 184, 52, 29, + 156, ]), }, { @@ -701,10 +709,10 @@ const permutedVectors: TestVector[] = [ purpose: Buffer.from([]), L: 64, okm: Buffer.from([ - 51, 160, 206, 70, 126, 207, 132, 181, 69, 177, 200, 56, 41, 184, 112, 236, 248, - 118, 1, 83, 102, 124, 10, 195, 119, 18, 79, 245, 95, 162, 181, 56, 81, 236, 177, - 189, 206, 96, 201, 81, 175, 224, 226, 29, 145, 43, 200, 85, 39, 114, 11, 109, - 149, 95, 25, 11, 115, 33, 203, 94, 254, 126, 91, 130 + 51, 160, 206, 70, 126, 207, 132, 181, 69, 177, 200, 56, 41, 184, 112, 236, + 248, 118, 1, 83, 102, 124, 10, 195, 119, 18, 79, 245, 95, 162, 181, 56, + 81, 236, 177, 189, 206, 96, 201, 81, 175, 224, 226, 29, 145, 43, 200, 85, + 39, 114, 11, 109, 149, 95, 25, 11, 115, 33, 203, 94, 254, 126, 91, 130, ]), }, { @@ -721,10 +729,11 @@ const permutedVectors: TestVector[] = [ purpose: Buffer.from([]), L: 64, okm: Buffer.from([ - 52, 56, 249, 137, 118, 241, 81, 3, 106, 148, 187, 233, 144, 50, 94, 163, 67, - 205, 109, 134, 183, 190, 143, 196, 219, 244, 120, 193, 244, 189, 86, 158, 109, - 109, 248, 231, 200, 58, 81, 17, 125, 98, 241, 66, 160, 75, 198, 179, 152, 75, - 78, 124, 238, 111, 9, 61, 193, 16, 48, 103, 202, 38, 129, 54 + 52, 56, 249, 137, 118, 241, 81, 3, 106, 148, 187, 233, 144, 50, 94, 163, + 67, 205, 109, 134, 183, 190, 143, 196, 219, 244, 120, 193, 244, 189, 86, + 158, 109, 109, 248, 231, 200, 58, 81, 17, 125, 98, 241, 66, 160, 75, 198, + 179, 152, 75, 78, 124, 238, 111, 9, 61, 193, 16, 48, 103, 202, 38, 129, + 54, ]), }, { @@ -738,10 +747,10 @@ const permutedVectors: TestVector[] = [ purpose: Buffer.from([0, 0, 0]), L: 64, okm: Buffer.from([ - 149, 140, 56, 1, 42, 245, 141, 18, 147, 234, 181, 53, 117, 134, 205, 36, 207, - 162, 134, 79, 181, 46, 106, 91, 151, 77, 66, 248, 56, 48, 162, 103, 1, 93, 196, - 85, 14, 201, 194, 108, 225, 190, 80, 221, 10, 156, 15, 109, 79, 39, 93, 240, - 17, 198, 82, 18, 26, 230, 140, 175, 200, 70, 243, 73 + 149, 140, 56, 1, 42, 245, 141, 18, 147, 234, 181, 53, 117, 134, 205, 36, + 207, 162, 134, 79, 181, 46, 106, 91, 151, 77, 66, 248, 56, 48, 162, 103, + 1, 93, 196, 85, 14, 201, 194, 108, 225, 190, 80, 221, 10, 156, 15, 109, + 79, 39, 93, 240, 17, 198, 82, 18, 26, 230, 140, 175, 200, 70, 243, 73, ]), }, { @@ -758,10 +767,10 @@ const permutedVectors: TestVector[] = [ purpose: Buffer.from([0, 0, 0]), L: 64, okm: Buffer.from([ - 246, 171, 10, 194, 229, 245, 91, 201, 41, 183, 21, 176, 40, 248, 85, 39, 158, - 233, 162, 152, 83, 192, 172, 239, 30, 97, 78, 140, 54, 168, 77, 243, 120, 159, - 194, 134, 42, 21, 121, 100, 184, 90, 95, 88, 14, 185, 208, 204, 216, 8, 65, 173, - 63, 200, 95, 76, 110, 57, 165, 239, 184, 191, 32, 129 + 246, 171, 10, 194, 229, 245, 91, 201, 41, 183, 21, 176, 40, 248, 85, 39, + 158, 233, 162, 152, 83, 192, 172, 239, 30, 97, 78, 140, 54, 168, 77, 243, + 120, 159, 194, 134, 42, 21, 121, 100, 184, 90, 95, 88, 14, 185, 208, 204, + 216, 8, 65, 173, 63, 200, 95, 76, 110, 57, 165, 239, 184, 191, 32, 129, ]), }, { @@ -776,8 +785,8 @@ const permutedVectors: TestVector[] = [ purpose: Buffer.from([]), L: 32, okm: Buffer.from([ - 204, 31, 218, 43, 73, 249, 90, 197, 128, 11, 55, 209, 21, 96, 149, 204, 65, 250, - 190, 210, 192, 112, 42, 120, 150, 31, 38, 171, 15, 85, 45, 253 + 204, 31, 218, 43, 73, 249, 90, 197, 128, 11, 55, 209, 21, 96, 149, 204, + 65, 250, 190, 210, 192, 112, 42, 120, 150, 31, 38, 171, 15, 85, 45, 253, ]), }, @@ -796,8 +805,9 @@ const permutedVectors: TestVector[] = [ purpose: Buffer.from([]), L: 32, okm: Buffer.from([ - 145, 220, 193, 19, 71, 197, 30, 204, 224, 63, 133, 97, 42, 121, 70, 123, 160, - 27, 123, 192, 190, 184, 13, 199, 127, 88, 162, 198, 152, 155, 202, 62 + 145, 220, 193, 19, 71, 197, 30, 204, 224, 63, 133, 97, 42, 121, 70, 123, + 160, 27, 123, 192, 190, 184, 13, 199, 127, 88, 162, 198, 152, 155, 202, + 62, ]), }, { @@ -812,8 +822,8 @@ const permutedVectors: TestVector[] = [ purpose: Buffer.from([0, 0, 0]), L: 32, okm: Buffer.from([ - 108, 227, 141, 225, 189, 36, 224, 42, 196, 111, 87, 108, 219, 47, 90, 54, 182, - 250, 91, 139, 155, 32, 44, 63, 224, 169, 27, 220, 89, 92, 198, 205 + 108, 227, 141, 225, 189, 36, 224, 42, 196, 111, 87, 108, 219, 47, 90, 54, + 182, 250, 91, 139, 155, 32, 44, 63, 224, 169, 27, 220, 89, 92, 198, 205, ]), }, { @@ -831,8 +841,8 @@ const permutedVectors: TestVector[] = [ purpose: Buffer.from([0, 0, 0]), L: 32, okm: Buffer.from([ - 39, 20, 91, 243, 52, 136, 88, 52, 191, 193, 105, 132, 150, 194, 213, 128, 27, - 212, 3, 35, 108, 50, 86, 137, 186, 61, 14, 15, 50, 249, 237, 218 + 39, 20, 91, 243, 52, 136, 88, 52, 191, 193, 105, 132, 150, 194, 213, 128, + 27, 212, 3, 35, 108, 50, 86, 137, 186, 61, 14, 15, 50, 249, 237, 218, ]), }, { @@ -847,10 +857,10 @@ const permutedVectors: TestVector[] = [ purpose: Buffer.from([]), L: 64, okm: Buffer.from([ - 111, 207, 165, 86, 103, 173, 136, 120, 48, 252, 135, 205, 83, 250, 1, 79, 142, - 95, 199, 64, 26, 206, 24, 96, 233, 181, 87, 170, 163, 188, 156, 143, 8, 154, - 91, 67, 105, 48, 15, 1, 231, 173, 105, 23, 41, 68, 46, 57, 82, 96, 81, 129, 143, - 163, 153, 143, 18, 153, 183, 105, 90, 18, 100, 213 + 111, 207, 165, 86, 103, 173, 136, 120, 48, 252, 135, 205, 83, 250, 1, 79, + 142, 95, 199, 64, 26, 206, 24, 96, 233, 181, 87, 170, 163, 188, 156, 143, + 8, 154, 91, 67, 105, 48, 15, 1, 231, 173, 105, 23, 41, 68, 46, 57, 82, 96, + 81, 129, 143, 163, 153, 143, 18, 153, 183, 105, 90, 18, 100, 213, ]), }, { @@ -868,10 +878,11 @@ const permutedVectors: TestVector[] = [ purpose: Buffer.from([]), L: 64, okm: Buffer.from([ - 236, 191, 183, 130, 155, 68, 57, 171, 78, 231, 92, 244, 117, 21, 240, 212, 82, - 203, 99, 236, 132, 223, 124, 228, 175, 255, 51, 30, 115, 36, 225, 148, 217, 75, - 221, 198, 255, 176, 106, 186, 171, 107, 242, 62, 100, 22, 37, 232, 90, 102, 1, - 114, 185, 237, 241, 8, 76, 132, 55, 93, 115, 68, 164, 129 + 236, 191, 183, 130, 155, 68, 57, 171, 78, 231, 92, 244, 117, 21, 240, 212, + 82, 203, 99, 236, 132, 223, 124, 228, 175, 255, 51, 30, 115, 36, 225, 148, + 217, 75, 221, 198, 255, 176, 106, 186, 171, 107, 242, 62, 100, 22, 37, + 232, 90, 102, 1, 114, 185, 237, 241, 8, 76, 132, 55, 93, 115, 68, 164, + 129, ]), }, { @@ -886,10 +897,10 @@ const permutedVectors: TestVector[] = [ purpose: Buffer.from([0, 0, 0]), L: 64, okm: Buffer.from([ - 189, 20, 161, 16, 60, 27, 20, 143, 166, 50, 26, 69, 192, 43, 37, 50, 129, 229, - 138, 138, 34, 32, 216, 44, 150, 238, 102, 123, 134, 164, 15, 27, 4, 71, 190, - 148, 21, 174, 103, 238, 40, 80, 205, 218, 227, 159, 207, 182, 62, 1, 157, 153, - 81, 86, 0, 11, 52, 106, 145, 54, 38, 205, 193, 114 + 189, 20, 161, 16, 60, 27, 20, 143, 166, 50, 26, 69, 192, 43, 37, 50, 129, + 229, 138, 138, 34, 32, 216, 44, 150, 238, 102, 123, 134, 164, 15, 27, 4, + 71, 190, 148, 21, 174, 103, 238, 40, 80, 205, 218, 227, 159, 207, 182, 62, + 1, 157, 153, 81, 86, 0, 11, 52, 106, 145, 54, 38, 205, 193, 114, ]), }, { @@ -907,10 +918,11 @@ const permutedVectors: TestVector[] = [ purpose: Buffer.from([0, 0, 0]), L: 64, okm: Buffer.from([ - 213, 189, 233, 32, 79, 28, 27, 234, 6, 242, 105, 186, 173, 11, 222, 183, 209, - 134, 176, 227, 134, 81, 243, 233, 197, 162, 253, 186, 173, 66, 61, 230, 175, - 170, 182, 122, 129, 114, 59, 179, 170, 150, 160, 116, 189, 167, 155, 45, 40, - 43, 77, 76, 250, 216, 177, 237, 203, 127, 53, 124, 82, 116, 231, 153 + 213, 189, 233, 32, 79, 28, 27, 234, 6, 242, 105, 186, 173, 11, 222, 183, + 209, 134, 176, 227, 134, 81, 243, 233, 197, 162, 253, 186, 173, 66, 61, + 230, 175, 170, 182, 122, 129, 114, 59, 179, 170, 150, 160, 116, 189, 167, + 155, 45, 40, 43, 77, 76, 250, 216, 177, 237, 203, 127, 53, 124, 82, 116, + 231, 153, ]), }, { @@ -924,8 +936,8 @@ const permutedVectors: TestVector[] = [ purpose: Buffer.from([]), L: 32, okm: Buffer.from([ - 204, 31, 218, 43, 73, 249, 90, 197, 128, 11, 55, 209, 21, 96, 149, 204, 65, 250, - 190, 210, 192, 112, 42, 120, 150, 31, 38, 171, 15, 85, 45, 253 + 204, 31, 218, 43, 73, 249, 90, 197, 128, 11, 55, 209, 21, 96, 149, 204, + 65, 250, 190, 210, 192, 112, 42, 120, 150, 31, 38, 171, 15, 85, 45, 253, ]), }, { @@ -942,8 +954,9 @@ const permutedVectors: TestVector[] = [ purpose: Buffer.from([]), L: 32, okm: Buffer.from([ - 145, 220, 193, 19, 71, 197, 30, 204, 224, 63, 133, 97, 42, 121, 70, 123, 160, - 27, 123, 192, 190, 184, 13, 199, 127, 88, 162, 198, 152, 155, 202, 62 + 145, 220, 193, 19, 71, 197, 30, 204, 224, 63, 133, 97, 42, 121, 70, 123, + 160, 27, 123, 192, 190, 184, 13, 199, 127, 88, 162, 198, 152, 155, 202, + 62, ]), }, { @@ -957,8 +970,8 @@ const permutedVectors: TestVector[] = [ purpose: Buffer.from([0, 0, 0]), L: 32, okm: Buffer.from([ - 108, 227, 141, 225, 189, 36, 224, 42, 196, 111, 87, 108, 219, 47, 90, 54, 182, - 250, 91, 139, 155, 32, 44, 63, 224, 169, 27, 220, 89, 92, 198, 205 + 108, 227, 141, 225, 189, 36, 224, 42, 196, 111, 87, 108, 219, 47, 90, 54, + 182, 250, 91, 139, 155, 32, 44, 63, 224, 169, 27, 220, 89, 92, 198, 205, ]), }, { @@ -975,8 +988,8 @@ const permutedVectors: TestVector[] = [ purpose: Buffer.from([0, 0, 0]), L: 32, okm: Buffer.from([ - 39, 20, 91, 243, 52, 136, 88, 52, 191, 193, 105, 132, 150, 194, 213, 128, 27, - 212, 3, 35, 108, 50, 86, 137, 186, 61, 14, 15, 50, 249, 237, 218 + 39, 20, 91, 243, 52, 136, 88, 52, 191, 193, 105, 132, 150, 194, 213, 128, + 27, 212, 3, 35, 108, 50, 86, 137, 186, 61, 14, 15, 50, 249, 237, 218, ]), }, { @@ -990,10 +1003,10 @@ const permutedVectors: TestVector[] = [ purpose: Buffer.from([]), L: 64, okm: Buffer.from([ - 111, 207, 165, 86, 103, 173, 136, 120, 48, 252, 135, 205, 83, 250, 1, 79, 142, - 95, 199, 64, 26, 206, 24, 96, 233, 181, 87, 170, 163, 188, 156, 143, 8, 154, - 91, 67, 105, 48, 15, 1, 231, 173, 105, 23, 41, 68, 46, 57, 82, 96, 81, 129, 143, - 163, 153, 143, 18, 153, 183, 105, 90, 18, 100, 213 + 111, 207, 165, 86, 103, 173, 136, 120, 48, 252, 135, 205, 83, 250, 1, 79, + 142, 95, 199, 64, 26, 206, 24, 96, 233, 181, 87, 170, 163, 188, 156, 143, + 8, 154, 91, 67, 105, 48, 15, 1, 231, 173, 105, 23, 41, 68, 46, 57, 82, 96, + 81, 129, 143, 163, 153, 143, 18, 153, 183, 105, 90, 18, 100, 213, ]), }, { @@ -1010,10 +1023,11 @@ const permutedVectors: TestVector[] = [ purpose: Buffer.from([]), L: 64, okm: Buffer.from([ - 236, 191, 183, 130, 155, 68, 57, 171, 78, 231, 92, 244, 117, 21, 240, 212, 82, - 203, 99, 236, 132, 223, 124, 228, 175, 255, 51, 30, 115, 36, 225, 148, 217, 75, - 221, 198, 255, 176, 106, 186, 171, 107, 242, 62, 100, 22, 37, 232, 90, 102, 1, - 114, 185, 237, 241, 8, 76, 132, 55, 93, 115, 68, 164, 129 + 236, 191, 183, 130, 155, 68, 57, 171, 78, 231, 92, 244, 117, 21, 240, 212, + 82, 203, 99, 236, 132, 223, 124, 228, 175, 255, 51, 30, 115, 36, 225, 148, + 217, 75, 221, 198, 255, 176, 106, 186, 171, 107, 242, 62, 100, 22, 37, + 232, 90, 102, 1, 114, 185, 237, 241, 8, 76, 132, 55, 93, 115, 68, 164, + 129, ]), }, { @@ -1027,10 +1041,10 @@ const permutedVectors: TestVector[] = [ purpose: Buffer.from([0, 0, 0]), L: 64, okm: Buffer.from([ - 189, 20, 161, 16, 60, 27, 20, 143, 166, 50, 26, 69, 192, 43, 37, 50, 129, 229, - 138, 138, 34, 32, 216, 44, 150, 238, 102, 123, 134, 164, 15, 27, 4, 71, 190, - 148, 21, 174, 103, 238, 40, 80, 205, 218, 227, 159, 207, 182, 62, 1, 157, 153, - 81, 86, 0, 11, 52, 106, 145, 54, 38, 205, 193, 114 + 189, 20, 161, 16, 60, 27, 20, 143, 166, 50, 26, 69, 192, 43, 37, 50, 129, + 229, 138, 138, 34, 32, 216, 44, 150, 238, 102, 123, 134, 164, 15, 27, 4, + 71, 190, 148, 21, 174, 103, 238, 40, 80, 205, 218, 227, 159, 207, 182, 62, + 1, 157, 153, 81, 86, 0, 11, 52, 106, 145, 54, 38, 205, 193, 114, ]), }, { @@ -1047,10 +1061,11 @@ const permutedVectors: TestVector[] = [ purpose: Buffer.from([0, 0, 0]), L: 64, okm: Buffer.from([ - 213, 189, 233, 32, 79, 28, 27, 234, 6, 242, 105, 186, 173, 11, 222, 183, 209, - 134, 176, 227, 134, 81, 243, 233, 197, 162, 253, 186, 173, 66, 61, 230, 175, - 170, 182, 122, 129, 114, 59, 179, 170, 150, 160, 116, 189, 167, 155, 45, 40, - 43, 77, 76, 250, 216, 177, 237, 203, 127, 53, 124, 82, 116, 231, 153 + 213, 189, 233, 32, 79, 28, 27, 234, 6, 242, 105, 186, 173, 11, 222, 183, + 209, 134, 176, 227, 134, 81, 243, 233, 197, 162, 253, 186, 173, 66, 61, + 230, 175, 170, 182, 122, 129, 114, 59, 179, 170, 150, 160, 116, 189, 167, + 155, 45, 40, 43, 77, 76, 250, 216, 177, 237, 203, 127, 53, 124, 82, 116, + 231, 153, ]), }, { @@ -1064,8 +1079,8 @@ const permutedVectors: TestVector[] = [ purpose: Buffer.from([]), L: 32, okm: Buffer.from([ - 204, 31, 218, 43, 73, 249, 90, 197, 128, 11, 55, 209, 21, 96, 149, 204, 65, 250, - 190, 210, 192, 112, 42, 120, 150, 31, 38, 171, 15, 85, 45, 253 + 204, 31, 218, 43, 73, 249, 90, 197, 128, 11, 55, 209, 21, 96, 149, 204, + 65, 250, 190, 210, 192, 112, 42, 120, 150, 31, 38, 171, 15, 85, 45, 253, ]), }, { @@ -1082,8 +1097,9 @@ const permutedVectors: TestVector[] = [ purpose: Buffer.from([]), L: 32, okm: Buffer.from([ - 145, 220, 193, 19, 71, 197, 30, 204, 224, 63, 133, 97, 42, 121, 70, 123, 160, - 27, 123, 192, 190, 184, 13, 199, 127, 88, 162, 198, 152, 155, 202, 62 + 145, 220, 193, 19, 71, 197, 30, 204, 224, 63, 133, 97, 42, 121, 70, 123, + 160, 27, 123, 192, 190, 184, 13, 199, 127, 88, 162, 198, 152, 155, 202, + 62, ]), }, { @@ -1097,8 +1113,8 @@ const permutedVectors: TestVector[] = [ purpose: Buffer.from([0, 0, 0]), L: 32, okm: Buffer.from([ - 108, 227, 141, 225, 189, 36, 224, 42, 196, 111, 87, 108, 219, 47, 90, 54, 182, - 250, 91, 139, 155, 32, 44, 63, 224, 169, 27, 220, 89, 92, 198, 205 + 108, 227, 141, 225, 189, 36, 224, 42, 196, 111, 87, 108, 219, 47, 90, 54, + 182, 250, 91, 139, 155, 32, 44, 63, 224, 169, 27, 220, 89, 92, 198, 205, ]), }, { @@ -1115,8 +1131,8 @@ const permutedVectors: TestVector[] = [ purpose: Buffer.from([0, 0, 0]), L: 32, okm: Buffer.from([ - 39, 20, 91, 243, 52, 136, 88, 52, 191, 193, 105, 132, 150, 194, 213, 128, 27, - 212, 3, 35, 108, 50, 86, 137, 186, 61, 14, 15, 50, 249, 237, 218 + 39, 20, 91, 243, 52, 136, 88, 52, 191, 193, 105, 132, 150, 194, 213, 128, + 27, 212, 3, 35, 108, 50, 86, 137, 186, 61, 14, 15, 50, 249, 237, 218, ]), }, { @@ -1130,10 +1146,10 @@ const permutedVectors: TestVector[] = [ purpose: Buffer.from([]), L: 64, okm: Buffer.from([ - 111, 207, 165, 86, 103, 173, 136, 120, 48, 252, 135, 205, 83, 250, 1, 79, 142, - 95, 199, 64, 26, 206, 24, 96, 233, 181, 87, 170, 163, 188, 156, 143, 8, 154, - 91, 67, 105, 48, 15, 1, 231, 173, 105, 23, 41, 68, 46, 57, 82, 96, 81, 129, 143, - 163, 153, 143, 18, 153, 183, 105, 90, 18, 100, 213 + 111, 207, 165, 86, 103, 173, 136, 120, 48, 252, 135, 205, 83, 250, 1, 79, + 142, 95, 199, 64, 26, 206, 24, 96, 233, 181, 87, 170, 163, 188, 156, 143, + 8, 154, 91, 67, 105, 48, 15, 1, 231, 173, 105, 23, 41, 68, 46, 57, 82, 96, + 81, 129, 143, 163, 153, 143, 18, 153, 183, 105, 90, 18, 100, 213, ]), }, { @@ -1150,10 +1166,11 @@ const permutedVectors: TestVector[] = [ purpose: Buffer.from([]), L: 64, okm: Buffer.from([ - 236, 191, 183, 130, 155, 68, 57, 171, 78, 231, 92, 244, 117, 21, 240, 212, 82, - 203, 99, 236, 132, 223, 124, 228, 175, 255, 51, 30, 115, 36, 225, 148, 217, 75, - 221, 198, 255, 176, 106, 186, 171, 107, 242, 62, 100, 22, 37, 232, 90, 102, 1, - 114, 185, 237, 241, 8, 76, 132, 55, 93, 115, 68, 164, 129 + 236, 191, 183, 130, 155, 68, 57, 171, 78, 231, 92, 244, 117, 21, 240, 212, + 82, 203, 99, 236, 132, 223, 124, 228, 175, 255, 51, 30, 115, 36, 225, 148, + 217, 75, 221, 198, 255, 176, 106, 186, 171, 107, 242, 62, 100, 22, 37, + 232, 90, 102, 1, 114, 185, 237, 241, 8, 76, 132, 55, 93, 115, 68, 164, + 129, ]), }, { @@ -1167,10 +1184,10 @@ const permutedVectors: TestVector[] = [ purpose: Buffer.from([0, 0, 0]), L: 64, okm: Buffer.from([ - 189, 20, 161, 16, 60, 27, 20, 143, 166, 50, 26, 69, 192, 43, 37, 50, 129, 229, - 138, 138, 34, 32, 216, 44, 150, 238, 102, 123, 134, 164, 15, 27, 4, 71, 190, - 148, 21, 174, 103, 238, 40, 80, 205, 218, 227, 159, 207, 182, 62, 1, 157, 153, - 81, 86, 0, 11, 52, 106, 145, 54, 38, 205, 193, 114 + 189, 20, 161, 16, 60, 27, 20, 143, 166, 50, 26, 69, 192, 43, 37, 50, 129, + 229, 138, 138, 34, 32, 216, 44, 150, 238, 102, 123, 134, 164, 15, 27, 4, + 71, 190, 148, 21, 174, 103, 238, 40, 80, 205, 218, 227, 159, 207, 182, 62, + 1, 157, 153, 81, 86, 0, 11, 52, 106, 145, 54, 38, 205, 193, 114, ]), }, { @@ -1187,10 +1204,11 @@ const permutedVectors: TestVector[] = [ purpose: Buffer.from([0, 0, 0]), L: 64, okm: Buffer.from([ - 213, 189, 233, 32, 79, 28, 27, 234, 6, 242, 105, 186, 173, 11, 222, 183, 209, - 134, 176, 227, 134, 81, 243, 233, 197, 162, 253, 186, 173, 66, 61, 230, 175, - 170, 182, 122, 129, 114, 59, 179, 170, 150, 160, 116, 189, 167, 155, 45, 40, - 43, 77, 76, 250, 216, 177, 237, 203, 127, 53, 124, 82, 116, 231, 153 + 213, 189, 233, 32, 79, 28, 27, 234, 6, 242, 105, 186, 173, 11, 222, 183, + 209, 134, 176, 227, 134, 81, 243, 233, 197, 162, 253, 186, 173, 66, 61, + 230, 175, 170, 182, 122, 129, 114, 59, 179, 170, 150, 160, 116, 189, 167, + 155, 45, 40, 43, 77, 76, 250, 216, 177, 237, 203, 127, 53, 124, 82, 116, + 231, 153, ]), }, ] @@ -1198,10 +1216,9 @@ const permutedVectors: TestVector[] = [ export const rawTestVectors = [b1, b2, b3, b4, b5, b6, b7, b8, b9, b10] export const testVectors = [c1, c2, c3, c4, c5, ...permutedVectors] - export const vectorOkmDigest = Buffer.from([ - 100, 105, 118, 112, 24, 213, 47, 164, 113, 176, 211, 130, 28, 237, 167, 5, 250, - 213, 40, 209, 195, 24, 247, 227, 48, 49, 159, 28, 32, 61, 178, 103 + 100, 105, 118, 112, 24, 213, 47, 164, 113, 176, 211, 130, 28, 237, 167, 5, + 250, 213, 40, 209, 195, 24, 247, 227, 48, 49, 159, 28, 32, 61, 178, 103, ]) export const testVectorDigest = () => diff --git a/modules/kdf-ctr-mode-node/test/kdfctr.test.ts b/modules/kdf-ctr-mode-node/test/kdfctr.test.ts index d935a5af2..2763fc4dc 100644 --- a/modules/kdf-ctr-mode-node/test/kdfctr.test.ts +++ b/modules/kdf-ctr-mode-node/test/kdfctr.test.ts @@ -43,8 +43,6 @@ describe('KDF Ctr Mode', () => { ).to.throw('The nonce must be provided') }) - - it('Precondition: the ikm must be 32, 48, 66 bytes long', () => { const invalidIkm = Buffer.alloc(31) expect(() => diff --git a/modules/kms-keyring-node/src/kms_hkeyring_node_helpers.ts b/modules/kms-keyring-node/src/kms_hkeyring_node_helpers.ts index 2ecb10c2d..d9b842d60 100644 --- a/modules/kms-keyring-node/src/kms_hkeyring_node_helpers.ts +++ b/modules/kms-keyring-node/src/kms_hkeyring_node_helpers.ts @@ -180,7 +180,6 @@ export function getCacheEntryId( ]) } - // hash the branch key id buffer with sha512 return createHash(CACHE_ENTRY_ID_DIGEST_ALGORITHM) .update(entryInfo) From 90d1fc30d5cdf71e215f1a16646341d105cc7bfc Mon Sep 17 00:00:00 2001 From: seebees Date: Wed, 15 Jan 2025 11:33:03 -0800 Subject: [PATCH 24/25] prefix is not required --- .../src/branch_keystore_helpers.ts | 17 +++++++++-------- .../test/branch_keystore_helpers.test.ts | 14 +++----------- 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/modules/branch-keystore-node/src/branch_keystore_helpers.ts b/modules/branch-keystore-node/src/branch_keystore_helpers.ts index 8616d92f8..4dedc48ef 100644 --- a/modules/branch-keystore-node/src/branch_keystore_helpers.ts +++ b/modules/branch-keystore-node/src/branch_keystore_helpers.ts @@ -165,17 +165,18 @@ export function validateBranchKeyRecord(item: BranchKeyItem): BranchKeyRecord { `Branch keystore record does not contain ${HIERARCHY_VERSION_FIELD} field of type number` ) + + // This requirement is around the construction of the encryption context. + // It is possible that customers will have constructed their own branch keys + // with a custom creation method. + // In this case encryption context may not be prefixed. + // The Dafny version of this code does not enforce + // that additional encryption context keys MUST be prefixed, + // therefore the JS release does not as well. + //= aws-encryption-sdk-specification/framework/key-store/dynamodb-key-storage.md#record-format //# A branch key record MAY include [custom encryption context](../branch-key-store.md#custom-encryption-context) key-value pairs. //# These attributes should be prefixed with `aws-crypto-ec:` the same way they are for [AWS KMS encryption context](../branch-key-store.md#encryption-context). - for (const field in item) { - if (!POTENTIAL_BRANCH_KEY_RECORD_FIELDS.includes(field)) { - needs( - field.startsWith(CUSTOM_ENCRYPTION_CONTEXT_FIELD_PREFIX), - `Custom encryption context key ${field} should be prefixed with ${CUSTOM_ENCRYPTION_CONTEXT_FIELD_PREFIX}` - ) - } - } // serialize the DDB response item as a more well-defined and validated branch // key record object diff --git a/modules/branch-keystore-node/test/branch_keystore_helpers.test.ts b/modules/branch-keystore-node/test/branch_keystore_helpers.test.ts index cb510079a..de2fb2855 100644 --- a/modules/branch-keystore-node/test/branch_keystore_helpers.test.ts +++ b/modules/branch-keystore-node/test/branch_keystore_helpers.test.ts @@ -333,26 +333,18 @@ describe('Test keystore helpers', () => { }) }) - it('Active & versioned items have additional fields prefixed improperly', () => { + it('Active & versioned items may have additional fields that are not prefixed', () => { const activeItem = { ...ACTIVE_BRANCH_KEY, ...INVALID_CUSTOM_ENCRYPTION_CONTEXT_KV_PAIRS, } - expect(() => validateBranchKeyRecord(activeItem)).to.throw( - `Custom encryption context key ${ - Object.keys(INVALID_CUSTOM_ENCRYPTION_CONTEXT_KV_PAIRS)[0] - } should be prefixed with ${CUSTOM_ENCRYPTION_CONTEXT_FIELD_PREFIX}` - ) + expect(() => validateBranchKeyRecord(activeItem)).to.not.throw() const versionedItem = { ...VERSION_BRANCH_KEY, ...INVALID_CUSTOM_ENCRYPTION_CONTEXT_KV_PAIRS, } - expect(() => validateBranchKeyRecord(versionedItem)).to.throw( - `Custom encryption context key ${ - Object.keys(INVALID_CUSTOM_ENCRYPTION_CONTEXT_KV_PAIRS)[0] - } should be prefixed with ${CUSTOM_ENCRYPTION_CONTEXT_FIELD_PREFIX}` - ) + expect(() => validateBranchKeyRecord(versionedItem)).to.not.throw() }) }) From d2c76af7e2390f24643b36218903d7e473507802 Mon Sep 17 00:00:00 2001 From: seebees Date: Wed, 15 Jan 2025 11:41:13 -0800 Subject: [PATCH 25/25] lint --- modules/branch-keystore-node/src/branch_keystore_helpers.ts | 2 -- .../branch-keystore-node/test/branch_keystore_helpers.test.ts | 1 - 2 files changed, 3 deletions(-) diff --git a/modules/branch-keystore-node/src/branch_keystore_helpers.ts b/modules/branch-keystore-node/src/branch_keystore_helpers.ts index 4dedc48ef..44cd5bdd5 100644 --- a/modules/branch-keystore-node/src/branch_keystore_helpers.ts +++ b/modules/branch-keystore-node/src/branch_keystore_helpers.ts @@ -28,7 +28,6 @@ import { BRANCH_KEY_TYPE_PREFIX, BRANCH_KEY_ACTIVE_TYPE, BEACON_KEY_TYPE_VALUE, - POTENTIAL_BRANCH_KEY_RECORD_FIELDS, } from './constants' /** @@ -165,7 +164,6 @@ export function validateBranchKeyRecord(item: BranchKeyItem): BranchKeyRecord { `Branch keystore record does not contain ${HIERARCHY_VERSION_FIELD} field of type number` ) - // This requirement is around the construction of the encryption context. // It is possible that customers will have constructed their own branch keys // with a custom creation method. diff --git a/modules/branch-keystore-node/test/branch_keystore_helpers.test.ts b/modules/branch-keystore-node/test/branch_keystore_helpers.test.ts index de2fb2855..142459bc0 100644 --- a/modules/branch-keystore-node/test/branch_keystore_helpers.test.ts +++ b/modules/branch-keystore-node/test/branch_keystore_helpers.test.ts @@ -31,7 +31,6 @@ import { BRANCH_KEY_FIELD, BRANCH_KEY_IDENTIFIER_FIELD, BRANCH_KEY_TYPE_PREFIX, - CUSTOM_ENCRYPTION_CONTEXT_FIELD_PREFIX, HIERARCHY_VERSION_FIELD, KEY_CREATE_TIME_FIELD, KMS_FIELD,