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..6fd8f886f 160000 --- a/aws-encryption-sdk-specification +++ b/aws-encryption-sdk-specification @@ -1 +1 @@ -Subproject commit c35fbd91b28303d69813119088c44b5006395eb4 +Subproject commit 6fd8f886f708afeb89bcfb2a618ca57bb2bd48cd 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..8dcd37975 --- /dev/null +++ b/modules/branch-keystore-node/src/branch_keystore.ts @@ -0,0 +1,696 @@ +// 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 + //= 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 +} + +//= 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' + ) + + /* Precondition: KMS Configuration must be provided. */ + 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 = 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 a 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. Expected ${this.logicalKeyStoreName}, found ${activeEncryptedBranchKey.encryptionContext[TABLE_FIELD]}.' + ) + + //= 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( + branchKeyVersion && typeof branchKeyVersion === 'string', + '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 a 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. Expected ${this.logicalKeyStoreName}, found ${encryptedBranchKey.encryptionContext[TABLE_FIELD}.' + ) + + //= 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..44cd5bdd5 --- /dev/null +++ b/modules/branch-keystore-node/src/branch_keystore_helpers.ts @@ -0,0 +1,359 @@ +// 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 { 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, +} 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 the DynamoDB table ${ddbTableName}.` + ) + // 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` + ) + + // 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). + + // 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..7e34620f5 --- /dev/null +++ b/modules/branch-keystore-node/src/dynamodb_key_storage.ts @@ -0,0 +1,220 @@ +// 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', + '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, + '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. 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. + 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..a8282121f --- /dev/null +++ b/modules/branch-keystore-node/src/kms_config.ts @@ -0,0 +1,246 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + isMultiRegionAwsKmsArn, + // getRegionFromIdentifier, + parseAwsKmsKeyArn, +} from '@aws-crypto/kms-keyring' +import { + constructArnInOtherRegion, + mrkAwareAwsKmsKeyIdCompare, + ParsedAwsKmsKeyArn, +} 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 +//# 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 | undefined + + /** + * 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) + /* 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.' + ) + + if (configType === 'string') { + /* Precondition: Only `discovery` is a valid string value */ + needs(config === 'discovery', 'Unexpected config shape') + } else if ( + 'identifier' in (config as any) || + 'mrkIdentifier' in (config as any) + ) { + const arn = + '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') + + //= 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) + readOnlyProperty(this, '_arn', arn) + } else if ('region' in (config as any)) { + readOnlyProperty(this, '_mrkRegion', (config as any).region) + } else { + needs(false, 'Unexpected config shape') + } + + Object.freeze(this) + } + + 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, + //# 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 undefined + } 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 isMultiRegionAwsKmsArn(parsedArn) + ? constructArnInOtherRegion(parsedArn, this._mrkRegion) + : otherArn + } 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..28e57a902 --- /dev/null +++ b/modules/branch-keystore-node/src/types.ts @@ -0,0 +1,299 @@ +// 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 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.' + ) + 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) { + //= 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', + 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( + 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..d581d8e3a --- /dev/null +++ b/modules/branch-keystore-node/test/branch_keystore.test.ts @@ -0,0 +1,753 @@ +// 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, + KMS_CLIENT_USER_AGENT, + 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 provided.', () => { + for (const kmsConfiguration of [...falseyValues, ...truthyValues]) { + expect( + () => + new BranchKeyStoreNode({ + storage: { ddbTableName: DDB_TABLE_NAME }, + logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, + kmsConfiguration: kmsConfiguration as any, + }) + ).to.throw( + /Unexpected config shape|Config must be a `discovery` or an object./ + ) + } + }) + + 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(() => { + return new BranchKeyStoreNode({ + storage: { ddbTableName: DDB_TABLE_NAME, ddbClient }, + logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, + kmsConfiguration: { identifier: KEY_ID }, + 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_ARN } + 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.kmsConfiguration._config).equals(kmsConfig) + }) + + it('Test valid config with no clients', () => { + const kmsClient = new KMSClient({}) + const ddbClient = new DynamoDBClient({}) + const kmsConfig = { identifier: KEY_ARN } + + // 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) + } + }) + + //= 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: { + ddbTableName: DDB_TABLE_NAME, + ddbClient: ddbClient as any, + }, + logicalKeyStoreName: LOGICAL_KEYSTORE_NAME, + kmsConfiguration: KMS_CONFIGURATION, + }) + + 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, + }, + 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.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') + }) + + //= 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, + 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#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 + //= 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 Key Store 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('Storage is immutable', () => { + expect(Object.isFrozen(BRANCH_KEYSTORE.storage)).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 + ) + + //= 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) + 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_ARN } + 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) + //= 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) + }) + + it('Test get branch key version', async () => { + const kmsClient = new KMSClient({}) + const ddbClient = new DynamoDBClient({}) + const kmsConfig = { identifier: KEY_ARN } + + 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_ARN } + + 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_ARN } + + 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_ARN } + + 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 the DynamoDB table ${DDB_TABLE_NAME}.` + )) + }) + + it('Test get active key with no clients', async () => { + const kmsConfig = { identifier: KEY_ARN } + 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..142459bc0 --- /dev/null +++ b/modules/branch-keystore-node/test/branch_keystore_helpers.test.ts @@ -0,0 +1,678 @@ +// 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, + 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 } 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 the DynamoDB table ${DDB_TABLE_NAME}.` + )) + + 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 the DynamoDB table ${DDB_TABLE_NAME}.` + )) + }) + + 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 the DynamoDB table ${DDB_TABLE_NAME}.` + )) + }) + }) + + 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 may have additional fields that are not prefixed', () => { + const activeItem = { + ...ACTIVE_BRANCH_KEY, + ...INVALID_CUSTOM_ENCRYPTION_CONTEXT_KV_PAIRS, + } + expect(() => validateBranchKeyRecord(activeItem)).to.not.throw() + + const versionedItem = { + ...VERSION_BRANCH_KEY, + ...INVALID_CUSTOM_ENCRYPTION_CONTEXT_KV_PAIRS, + } + expect(() => validateBranchKeyRecord(versionedItem)).to.not.throw() + }) + }) + + // = 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 + //# - [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) + + 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. + 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..0dd77f435 --- /dev/null +++ b/modules/branch-keystore-node/test/kms_config.test.ts @@ -0,0 +1,259 @@ +// 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: 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({ identifier: arn } as any)).to.throw( + 'ARN must be a string' + ) + expect(() => supplySrkKmsConfig({ mrkIdentifier: 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({ mrkIdentifier: 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(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(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(ONE_PART_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(ONE_PART_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/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..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 @@ -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). 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/decrypt-node/test/compatibility.test.ts b/modules/decrypt-node/test/compatibility.test.ts index 8108522b4..7ab6847cb 100644 --- a/modules/decrypt-node/test/compatibility.test.ts +++ b/modules/decrypt-node/test/compatibility.test.ts @@ -12,53 +12,39 @@ 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' 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 +53,174 @@ 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 = 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, { + 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': + return new (class TestKeyring extends KeyringNode { + async _onEncrypt(): Promise { + throw new Error('I should never see this error') + } + async _onDecrypt(material: NodeDecryptionMaterial) { + const unencryptedDataKey = Buffer.alloc( + 32, + test['decrypted-dek'], + 'base64' + ) + 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 prescriptive, + // but the expectation is to be able to deprecate this + // in favor of the test vectors project (integration-node) + 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() { + return { + logicalName: 'logicalKeyStoreName', + } + }, + }, + } as any, + }) } - 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: + '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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: '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: + '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', + 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/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..4b71821a3 --- /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 YELLOW_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..7225115ff --- /dev/null +++ b/modules/kdf-ctr-mode-node/README.md @@ -0,0 +1,47 @@ +# @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 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 + +```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..bc9718b3c --- /dev/null +++ b/modules/kdf-ctr-mode-node/src/kdfctr.ts @@ -0,0 +1,158 @@ +// 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, 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 | 64 + +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, 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 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) && + expectedLength * 8 < INT32_MAX_LIMIT && + expectedLength * 8 > 0, + `Unsupported requested length ${expectedLength}` + ) + + const label = purpose || 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 */ + 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.floor((length + h - 1) / h) + + let buffer = Buffer.alloc(0) + let i = Buffer.from(uInt32BE(COUNTER_START_VALUE)) + + for (let iteration = 1; iteration <= iterations; 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/fixtures.ts b/modules/kdf-ctr-mode-node/test/fixtures.ts new file mode 100644 index 000000000..93371f578 --- /dev/null +++ b/modules/kdf-ctr-mode-node/test/fixtures.ts @@ -0,0 +1,1241 @@ +// 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 +} + +export 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. + +// 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 + +// 2 * 3 * 2 * 2 * 2 == 48 + +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, + ]), + }, + + { + 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, ...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, +]) + +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 new file mode 100644 index 000000000..2763fc4dc --- /dev/null +++ b/modules/kdf-ctr-mode-node/test/kdfctr.test.ts @@ -0,0 +1,352 @@ +// 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, TestVector } from './fixtures' +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, 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({ + digestAlgorithm, + ikm, + nonce, + purpose, + expectedLength: invalidExpectedLength, + }) + ).to.throw(`Unsupported requested length ${invalidExpectedLength}`) + + expect(() => + kdfCounterMode({ + digestAlgorithm, + ikm, + nonce, + purpose, + 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', () => { + 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 } = 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) +} 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..802b7d846 100644 --- a/modules/kms-keyring-node/package.json +++ b/modules/kms-keyring-node/package.json @@ -19,8 +19,13 @@ }, "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" }, 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..d9b842d60 --- /dev/null +++ b/modules/kms-keyring-node/src/kms_hkeyring_node_helpers.ts @@ -0,0 +1,586 @@ +// 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 { CryptographicMaterialsCache } from '@aws-crypto/cache-material' +import { kdfCounterMode } from '@aws-crypto/kdf-ctr-mode-node' +import { + CACHE_ENTRY_ID_DIGEST_ALGORITHM, + 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) + +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) + + 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) { + //= 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, + ]) + } + + // hash 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) + : // 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 + //# 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..16cd9935f --- /dev/null +++ b/modules/kms-keyring-node/test/fixtures.ts @@ -0,0 +1,54 @@ +// 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..bc4a0a22d --- /dev/null +++ b/modules/kms-keyring-node/test/kms_hkeyring_node.constructor.test.ts @@ -0,0 +1,290 @@ +// 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('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..d8d059818 --- /dev/null +++ b/modules/kms-keyring-node/test/kms_hkeyring_node.edk-order.test.ts @@ -0,0 +1,326 @@ +// 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..d870c7ec9 --- /dev/null +++ b/modules/kms-keyring-node/test/kms_hkeyring_node.ondecrypt.test.ts @@ -0,0 +1,636 @@ +// 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) + ) + } + }) + }) + + 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) + }) + + 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..436ece415 --- /dev/null +++ b/modules/kms-keyring-node/test/kms_hkeyring_node.onencrypt.test.ts @@ -0,0 +1,419 @@ +// 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 + //= 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) + //# 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 + //# 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). + 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/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', 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..4b5cdede2 100644 --- a/modules/kms-keyring/src/index.ts +++ b/modules/kms-keyring/src/index.ts @@ -2,6 +2,14 @@ // SPDX-License-Identifier: Apache-2.0 export * from './kms_client_supplier' +export { + getRegionFromIdentifier, + parseAwsKmsKeyArn, + constructArnInOtherRegion, + mrkAwareAwsKmsKeyIdCompare, + isMultiRegionAwsKmsArn, + ParsedAwsKmsKeyArn, +} from './arn_parsing' export * from './kms_keyring' export * from './kms_mrk_keyring' export * from './kms_mrk_discovery_keyring' @@ -10,3 +18,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..a200dfa88 --- /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/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-lock.json b/package-lock.json index be9ea1e4c..ad4ff6e15 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,8 @@ "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", "typescript": "^4.4.3", @@ -86,6 +90,17 @@ "webpack-cli": "^4.7.2" } }, + "modules/branch-keystore-node": { + "name": "@aws-crypto/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 +179,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 +510,17 @@ "yauzl": "^2.10.0" } }, + "modules/kdf-ctr-mode-node": { + "name": "@aws-crypto/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 +547,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 +669,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 +735,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 +799,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 +991,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 +1864,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 +2071,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", @@ -3899,13 +4531,56 @@ "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.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 +4588,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 +4603,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 +4621,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 +4648,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 +4662,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 +4683,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 +4696,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 +4714,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 +4733,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 +4745,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 +4757,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 +4771,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 +4786,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 +4798,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 +4810,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 +4823,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 +4835,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 +4858,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 +4876,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 +4893,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 +4904,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 +4974,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 +4989,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 +5006,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 +5031,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 +5043,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 +5056,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 +5073,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 +5110,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 +5407,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 +5437,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", @@ -12287,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", @@ -13387,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", @@ -14393,6 +15109,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", @@ -14746,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", @@ -15743,6 +16489,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 +18694,39 @@ "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", + "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 7ca9df489..4d5a0ed07 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "author": "aws-crypto-tools-team@amazon.com", "license": "Apache-2.0", "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", @@ -79,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", @@ -132,6 +134,8 @@ "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", "typescript": "^4.4.3", 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, } }