diff --git a/NOTICE b/NOTICE index a95284f35..88f7bea1e 100644 --- a/NOTICE +++ b/NOTICE @@ -1,2 +1,2 @@ AWS Encryption SDK for Javascript -Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. +Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/modules/kms-keyring/LICENSE b/modules/kms-keyring/LICENSE new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/modules/kms-keyring/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 [yyyy] [name of copyright owner] + + 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/kms-keyring/NOTICE b/modules/kms-keyring/NOTICE new file mode 100644 index 000000000..88f7bea1e --- /dev/null +++ b/modules/kms-keyring/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/kms-keyring/package.json b/modules/kms-keyring/package.json new file mode 100644 index 000000000..c4b16eccf --- /dev/null +++ b/modules/kms-keyring/package.json @@ -0,0 +1,50 @@ +{ + "name": "@aws-crypto/kms-keyring", + "private": true, + "version": "0.0.1", + "scripts": { + "prepublishOnly": "npm run build", + "build": "tsc -b tsconfig.json && tsc -b tsconfig.module.json", + "lint": "standard 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/awslabs/aws-encryption-sdk-javascript" + }, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/material-management": "^0.1.0", + "@aws-sdk/types": "0.1.0-preview.1", + "tslib": "^1.9.3" + }, + "devDependencies": { + "@types/chai": "^4.1.4", + "@types/mocha": "^5.2.5", + "@types/node": "^11.11.4", + "@types/chai-as-promised": "^7.1.0", + "@typescript-eslint/eslint-plugin": "^1.4.2", + "@typescript-eslint/parser": "^1.4.2", + "chai": "^4.1.2", + "chai-as-promised": "^7.1.1", + "mocha": "^5.2.0", + "nyc": "^12.0.2", + "standard": "^12.0.1", + "ts-node": "^7.0.1", + "typescript": "^3.2.0" + }, + "sideEffects": false, + "main": "./build/main/index.js", + "module": "./build/module/index.js", + "types": "./build/main/index.d.ts", + "files": ["./build/**/*"], + "standard": { + "parser": "@typescript-eslint/parser", + "plugins": [ + "@typescript-eslint" + ] + } +} diff --git a/modules/kms-keyring/src/helpers.ts b/modules/kms-keyring/src/helpers.ts new file mode 100644 index 000000000..036b50517 --- /dev/null +++ b/modules/kms-keyring/src/helpers.ts @@ -0,0 +1,106 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use + * this file except in compliance with the License. A copy of the License is + * located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { KmsClientSupplier } from './kms_client_supplier' // eslint-disable-line no-unused-vars +import { GenerateDataKeyOutput } from './kms_types/GenerateDataKeyOutput' // eslint-disable-line no-unused-vars +import { DecryptOutput } from './kms_types/DecryptOutput' // eslint-disable-line no-unused-vars +import { EncryptOutput } from './kms_types/EncryptOutput' // eslint-disable-line no-unused-vars +import { KMS } from './kms_types/KMS' // eslint-disable-line no-unused-vars +import { regionFromKmsKeyArn } from './region_from_kms_key_arn' +import { EncryptionContext, EncryptedDataKey, needs } from '@aws-crypto/material-management' // eslint-disable-line no-unused-vars + +export const KMS_PROVIDER_ID = 'aws-kms' + +export async function generateDataKey ( + clientProvider: KmsClientSupplier, + NumberOfBytes: number, + KeyId: string, + EncryptionContext?: EncryptionContext, + GrantTokens?: string +) { + const region = regionFromKmsKeyArn(KeyId) + const client = clientProvider(region) + + /* Check for early return (Postcondition): Client region was not provided. */ + if (!client) return false + + const dataKey = await client.generateDataKey({ KeyId, GrantTokens, NumberOfBytes, EncryptionContext }) + + /* Postcondition: KMS must return serializable generate data key. */ + if (!isRequiredGenerateDataKeyOutput(dataKey)) throw new Error('Malformed KMS response.') + return dataKey +} + +export async function encrypt ( + clientProvider: KmsClientSupplier, + Plaintext: Uint8Array, + KeyId: string, + EncryptionContext?: EncryptionContext, + GrantTokens?: string +): Promise|false> { + const region = regionFromKmsKeyArn(KeyId) + const client = clientProvider(region) + + /* Check for early return (Postcondition): Client region was not provided. */ + if (!client) return false + + const kmsEDK = await client.encrypt({ KeyId, Plaintext, EncryptionContext, GrantTokens }) + + /* Postcondition: KMS must return serializable encrypted data key. */ + if (!isRequiredEncryptOutput(kmsEDK)) throw new Error('Malformed KMS response.') + return kmsEDK +} + +export async function decrypt ( + clientProvider: KmsClientSupplier, + edk: EncryptedDataKey, + EncryptionContext?: EncryptionContext, + GrantTokens?: string +): Promise|false> { + /* Precondition: The EDK must be a KMS edk. */ + needs(edk.providerId === KMS_PROVIDER_ID, 'Unsupported providerId') + const region = regionFromKmsKeyArn(edk.providerInfo) + const client = clientProvider(region) + /* Check for early return (Postcondition): Client region was not provided. */ + if (!client) return false + + const dataKey = await client.decrypt({ CiphertextBlob: edk.encryptedDataKey, EncryptionContext, GrantTokens }) + + /* Postcondition: KMS must return usable decrypted key. */ + if (!isRequiredDecryptOutput(dataKey)) throw new Error('Malformed KMS response.') + return dataKey +} + +export function kms2EncryptedDataKey ({ KeyId: providerInfo, CiphertextBlob: encryptedDataKey }: Required) { + return new EncryptedDataKey({ providerId: KMS_PROVIDER_ID, providerInfo, encryptedDataKey }) +} + +function isRequiredGenerateDataKeyOutput ( + dataKey: T +): dataKey is Required { + return !!dataKey.Plaintext && !!dataKey.KeyId && !!dataKey.CiphertextBlob +} + +function isRequiredEncryptOutput ( + kmsEDK: T +): kmsEDK is Required { + return !!kmsEDK.KeyId && !!kmsEDK.CiphertextBlob +} + +function isRequiredDecryptOutput ( + dataKey: T +): dataKey is Required { + return !!dataKey.KeyId && !!dataKey.Plaintext +} diff --git a/modules/kms-keyring/src/index.ts b/modules/kms-keyring/src/index.ts new file mode 100644 index 000000000..88b5fdd8a --- /dev/null +++ b/modules/kms-keyring/src/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use + * this file except in compliance with the License. A copy of the License is + * located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './kms_client_supplier' +export * from './kms_keyring' +export * from './helpers' +export * from './region_from_kms_key_arn' diff --git a/modules/kms-keyring/src/kms_client_supplier.ts b/modules/kms-keyring/src/kms_client_supplier.ts new file mode 100644 index 000000000..b30dbb87e --- /dev/null +++ b/modules/kms-keyring/src/kms_client_supplier.ts @@ -0,0 +1,115 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use + * this file except in compliance with the License. A copy of the License is + * located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { KMS } from './kms_types/KMS' // eslint-disable-line no-unused-vars +import { KMSConfiguration } from './kms_types/KMSConfiguration' // eslint-disable-line no-unused-vars +import { needs } from '@aws-crypto/material-management' + +export interface KMSConstructible { + new(config: Config) : Client +} + +export interface KmsClientSupplier { + /* KmsClientProvider is allowed to return undefined if, for example, user wants to exclude particular regions. */ + (region: string): Client|false +} + +export function getClient ( + KMSClient: KMSConstructible +): KmsClientSupplier { + return function getKmsClient (region: string) { + /* Precondition: region be a string. */ + needs(region && typeof region === 'string', 'A region is required') + + return new KMSClient({ region } as Config) + } +} + +export function limitRegions ( + regions: string[], + getClient: KmsClientSupplier +): KmsClientSupplier { + /* Precondition: region be a string. */ + needs(regions.every(r => !!r && typeof r === 'string'), 'Can only limit on region strings') + + return (region: string) => { + if (!regions.includes(region)) return false + return getClient(region) + } +} + +export function excludeRegions ( + regions: string[], + getClient: KmsClientSupplier +): KmsClientSupplier { + /* Precondition: region be a string. */ + needs(regions.every(r => !!r && typeof r === 'string'), 'Can only exclude on region strings') + + return (region: string) => { + if (regions.includes(region)) return false + return getClient(region) + } +} + +export function cacheClients ( + getClient: KmsClientSupplier +): KmsClientSupplier { + const clientsCache: {[key: string]: Client|false} = {} + + return (region: string) => { + // Do not cache until KMS has been responded in the given region + if (!clientsCache.hasOwnProperty(region)) return deferCache(clientsCache, region, getClient(region)) + return clientsCache[region] + } +} + +type KMSOperations = keyof KMS +/* It is possible that a malicious user can attempt a local resource + * DOS by sending ciphertext with a large number of spurious regions. + * This will fill the cache with regions and exhaust resources. + * To avoid this, a call succeeds in contacting KMS. + * This does *not* mean that this call is successful, + * only that the region is backed by a functional KMS service. + */ +function deferCache ( + clientsCache: {[key: string]: Client|false}, + region: string, + client: Client|false +): Client|false { + /* Check for early return (Postcondition): No client, then I cache false and move on. */ + if (!client) { + clientsCache[region] = false + return false + } + const { encrypt, decrypt, generateDataKey } = client + + return (['encrypt', 'decrypt', 'generateDataKey']).reduce(wrapOperation, client) + + /* Wrap each of the operations to cache the client on response */ + function wrapOperation (client: Client, name: KMSOperations): Client { + type params = Parameters + type retValue = ReturnType + const original = client[name] + client[name] = function (...args: params): retValue { + // @ts-ignore (there should be a TypeScript solution for this) + return original.apply(client, args) + .then((response: any) => { + clientsCache[region] = Object.assign(client, { encrypt, decrypt, generateDataKey }) + return response + }) + } + return client + } +} diff --git a/modules/kms-keyring/src/kms_keyring.ts b/modules/kms-keyring/src/kms_keyring.ts new file mode 100644 index 000000000..f63805771 --- /dev/null +++ b/modules/kms-keyring/src/kms_keyring.ts @@ -0,0 +1,192 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use + * this file except in compliance with the License. A copy of the License is + * located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { KmsClientSupplier } from './kms_client_supplier' // eslint-disable-line no-unused-vars +import { + needs, + Keyring, // eslint-disable-line no-unused-vars + EncryptionMaterial, // eslint-disable-line no-unused-vars + DecryptionMaterial, // eslint-disable-line no-unused-vars + SupportedAlgorithmSuites, // eslint-disable-line no-unused-vars + EncryptionContext, // eslint-disable-line no-unused-vars + KeyringTrace, // eslint-disable-line no-unused-vars + KeyringTraceFlag, + EncryptedDataKey, // eslint-disable-line no-unused-vars + immutableClass, + readOnlyProperty +} from '@aws-crypto/material-management' +import { KMS_PROVIDER_ID, generateDataKey, encrypt, decrypt, kms2EncryptedDataKey } from './helpers' +import { KMS } from './kms_types/KMS' // eslint-disable-line no-unused-vars +import { DecryptOutput } from './kms_types/DecryptOutput' // eslint-disable-line no-unused-vars +import { regionFromKmsKeyArn } from './region_from_kms_key_arn' + +export interface KmsKeyringInput { + clientProvider: KmsClientSupplier + keyIds?: string[] + generatorKeyId?: string + grantTokens?: string + discovery?: boolean +} + +export interface KeyRing extends Keyring { + keyIds: ReadonlyArray + generatorKeyId?: string + clientProvider: KmsClientSupplier + grantTokens?: string + isDiscovery: boolean + _onEncrypt(material: EncryptionMaterial, context?: EncryptionContext): Promise> + _onDecrypt(material: DecryptionMaterial, encryptedDataKeys: EncryptedDataKey[], context?: EncryptionContext): Promise> +} + +export interface KmsKeyRingConstructible { + new(input: KmsKeyringInput): KeyRing +} + +export interface KeyRingConstructible { + new(): Keyring +} + +export function KmsKeyringClass ( + BaseKeyring: KeyRingConstructible +): KmsKeyRingConstructible { + class KmsKeyring extends BaseKeyring implements KeyRing { + public keyIds!: ReadonlyArray + public generatorKeyId?: string + public clientProvider!: KmsClientSupplier + public grantTokens?: string + public isDiscovery!: boolean + + constructor ({ clientProvider, generatorKeyId, keyIds = [], grantTokens, discovery }: KmsKeyringInput) { + super() + /* Precondition: This is an abstract class. (But TypeScript does not have a clean way to model this) */ + needs(this.constructor !== KmsKeyring, 'new KmsKeyring is not allowed') + /* Precondition: A noop KmsKeyring is not allowed. */ + needs(!discovery && !generatorKeyId && !keyIds.length, 'Noop keyring is not allowed: Set a keyId or discovery') + /* Precondition: A keyring can be either a Discovery or have keyIds configured. */ + needs(discovery && (generatorKeyId || keyIds.length), 'A keyring can be either a Discovery or have keyIds configured.') + /* Precondition: All KMS key arns must be valid. */ + needs(!generatorKeyId || !!regionFromKmsKeyArn(generatorKeyId), 'Malformed arn.') + needs(keyIds.every(keyarn => !!regionFromKmsKeyArn(keyarn)), 'Malformed arn.') + /* Precondition: clientProvider needs to be a callable function. */ + needs(typeof clientProvider === 'function', 'Missing clientProvider') + + readOnlyProperty(this, 'clientProvider', clientProvider) + readOnlyProperty(this, 'keyIds', Object.freeze(keyIds.slice())) + readOnlyProperty(this, 'generatorKeyId', generatorKeyId) + readOnlyProperty(this, 'grantTokens', grantTokens) + readOnlyProperty(this, 'isDiscovery', !!discovery) + } + + /* Keyrings *must* preserve the order of EDK's. The generatorKeyId is the first on this list. */ + async _onEncrypt (material: EncryptionMaterial, context?: EncryptionContext) { + /* Check for early return (Postcondition): Discovery Keyrings do not encrypt. */ + if (this.isDiscovery) return material + + const keyIds = this.keyIds.slice() + const { clientProvider, generatorKeyId, grantTokens } = this + if (generatorKeyId && !material.hasUnencryptedDataKey) { + const dataKey = await generateDataKey(clientProvider, material.suite.keyLengthBytes, generatorKeyId, context, grantTokens) + /* Precondition: A generatorKeyId must generate if we do not have an unencrypted data key. + * Client supplier is allowed to return undefined if, for example, user wants to exclude particular + * regions. But if we are here it means that user configured keyring with a KMS key that was + * incompatible with the client supplier in use. + */ + if (!dataKey) throw new Error('Generator KMS key did not generate a data key') + + const flags = KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY | + KeyringTraceFlag.WRAPPING_KEY_SIGNED_ENC_CTX | + KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY + const trace: KeyringTrace = { keyNamespace: KMS_PROVIDER_ID, keyName: dataKey.KeyId, flags } + + material + /* Postcondition: The generated unencryptedDataKey length must match the algorithm specification. + * See cryptographic_materials as setUnencryptedDataKey will throw in this case. + */ + .setUnencryptedDataKey(dataKey.Plaintext, trace) + .addEncryptedDataKey(kms2EncryptedDataKey(dataKey), KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY) + } else if (generatorKeyId) { + keyIds.unshift(generatorKeyId) + } + + /* Precondition: If a generator does not exist, an unencryptedDataKey *must* already exist. + * Furthermore *only* CMK's explicitly designated as generators can generate data keys. + * See cryptographic_materials as getUnencryptedDataKey will throw in this case. + */ + const unencryptedDataKey = material.getUnencryptedDataKey() + + const flags = KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY | KeyringTraceFlag.WRAPPING_KEY_SIGNED_ENC_CTX + for (const kmsKey of keyIds) { + const kmsEDK = await encrypt(clientProvider, unencryptedDataKey, kmsKey, context, grantTokens) + + /* clientProvider may not return a client, in this case there is not an EDK to add */ + if (kmsEDK) material.addEncryptedDataKey(kms2EncryptedDataKey(kmsEDK), flags) + } + + return material + } + + async _onDecrypt (material: DecryptionMaterial, encryptedDataKeys: EncryptedDataKey[], context?: EncryptionContext) { + const keyIds = this.keyIds.slice() + const { clientProvider, generatorKeyId, grantTokens } = this + if (generatorKeyId) keyIds.unshift(generatorKeyId) + + /* If there are no key IDs in the list, keyring is in "discovery" mode and will attempt KMS calls with + * every ARN it comes across in the message. If there are key IDs in the list, it will cross check the + * ARN it reads with that list before attempting KMS calls. Note that if caller provided key IDs in + * anything other than a CMK ARN format, the SDK will not attempt to decrypt those data keys, because + * the EDK data format always specifies the CMK with the full (non-alias) ARN. + */ + const decryptableEDKs = encryptedDataKeys + .filter(({ providerId, providerInfo }) => { + if (providerId !== KMS_PROVIDER_ID) return false + /* Discovery keyrings can not have keyIds configured, + * and non-discovery keyrings must have keyIds configured. + */ + return this.isDiscovery || keyIds.includes(providerInfo) + }) + + for (const edk of decryptableEDKs) { + let dataKey: Required|false = false + try { + dataKey = await decrypt(clientProvider, edk, context, grantTokens) + } catch (e) { + // there should be some debug here? or wrap? + // Failures decrypt should not short-circuit the process + // If the caller does not have access they may have access + // through another Keyring. + } + + /* Check for early return (Postcondition): clientProvider may not return a client. */ + if (!dataKey) continue + + /* Postcondition: The KeyId from KMS must match the encoded KeyID. */ + needs(dataKey.KeyId === edk.providerInfo, 'KMS Decryption key does not match serialized provider.') + + const flags = KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY | KeyringTraceFlag.WRAPPING_KEY_VERIFIED_ENC_CTX + const trace: KeyringTrace = { keyNamespace: KMS_PROVIDER_ID, keyName: dataKey.KeyId, flags } + + /* Postcondition: The decrypted unencryptedDataKey length must match the algorithm specification. + * See cryptographic_materials as setUnencryptedDataKey will throw in this case. + */ + material.setUnencryptedDataKey(dataKey.Plaintext, trace) + return material + } + + return material + } + } + immutableClass(KmsKeyring) + return KmsKeyring +} diff --git a/modules/kms-keyring/src/kms_types/DecryptInput.ts b/modules/kms-keyring/src/kms_types/DecryptInput.ts new file mode 100644 index 000000000..60f9462a9 --- /dev/null +++ b/modules/kms-keyring/src/kms_types/DecryptInput.ts @@ -0,0 +1,39 @@ +import {NodeHttpOptions, BrowserHttpOptions} from '@aws-sdk/types'; +import * as __aws_sdk_types from '@aws-sdk/types'; + +/** + * DecryptInput shape + */ +export interface DecryptInput { + /** + *

Ciphertext to be decrypted. The blob includes metadata.

+ */ + CiphertextBlob: ArrayBuffer|ArrayBufferView|string; + + /** + *

The encryption context. If this was specified in the Encrypt function, it must be specified here or the decryption operation will fail. For more information, see Encryption Context.

+ */ + EncryptionContext?: {[key: string]: string}|Iterable<[string, string]>; + + /** + *

A list of grant tokens.

For more information, see Grant Tokens in the AWS Key Management Service Developer Guide.

+ */ + GrantTokens?: Array|Iterable; + + /** + * The maximum number of times this operation should be retried. If set, this value will override the `maxRetries` configuration set on the client for this command. + */ + $maxRetries?: number; + + /** + * An object that may be queried to determine if the underlying operation has been aborted. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal + */ + $abortSignal?: __aws_sdk_types.AbortSignal; + + /** + * Per-request HTTP configuration options. If set, any options specified will override the corresponding HTTP option set on the client for this command. + */ + $httpOptions?: NodeHttpOptions|BrowserHttpOptions; +} \ No newline at end of file diff --git a/modules/kms-keyring/src/kms_types/DecryptOutput.ts b/modules/kms-keyring/src/kms_types/DecryptOutput.ts new file mode 100644 index 000000000..61eb4b6f6 --- /dev/null +++ b/modules/kms-keyring/src/kms_types/DecryptOutput.ts @@ -0,0 +1,21 @@ +import * as __aws_sdk_types from '@aws-sdk/types'; + +/** + * DecryptOutput shape + */ +export interface DecryptOutput { + /** + *

ARN of the key used to perform the decryption. This value is returned if no errors are encountered during the operation.

+ */ + KeyId?: string; + + /** + *

Decrypted plaintext data. When you use the HTTP API or the AWS CLI, the value is Base64-encoded. Otherwise, it is not encoded.

+ */ + Plaintext?: Uint8Array; + + /** + * Metadata about the response received, including the HTTP status code, HTTP headers, and any request identifiers recognized by the SDK. + */ + $metadata: __aws_sdk_types.ResponseMetadata; +} diff --git a/modules/kms-keyring/src/kms_types/EncryptInput.ts b/modules/kms-keyring/src/kms_types/EncryptInput.ts new file mode 100644 index 000000000..3f6051d61 --- /dev/null +++ b/modules/kms-keyring/src/kms_types/EncryptInput.ts @@ -0,0 +1,44 @@ +import {NodeHttpOptions, BrowserHttpOptions} from '@aws-sdk/types'; +import * as __aws_sdk_types from '@aws-sdk/types'; + +/** + * EncryptInput shape + */ +export interface EncryptInput { + /** + *

A unique identifier for the customer master key (CMK).

To specify a CMK, use its key ID, Amazon Resource Name (ARN), alias name, or alias ARN. When using an alias name, prefix it with "alias/". To specify a CMK in a different AWS account, you must use the key ARN or alias ARN.

For example:

  • Key ID: 1234abcd-12ab-34cd-56ef-1234567890ab

  • Key ARN: arn:aws:kms:us-east-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab

  • Alias name: alias/ExampleAlias

  • Alias ARN: arn:aws:kms:us-east-2:111122223333:alias/ExampleAlias

To get the key ID and key ARN for a CMK, use ListKeys or DescribeKey. To get the alias name and alias ARN, use ListAliases.

+ */ + KeyId: string; + + /** + *

Data to be encrypted.

+ */ + Plaintext: ArrayBuffer|ArrayBufferView|string; + + /** + *

Name-value pair that specifies the encryption context to be used for authenticated encryption. If used here, the same value must be supplied to the Decrypt API or decryption will fail. For more information, see Encryption Context.

+ */ + EncryptionContext?: {[key: string]: string}|Iterable<[string, string]>; + + /** + *

A list of grant tokens.

For more information, see Grant Tokens in the AWS Key Management Service Developer Guide.

+ */ + GrantTokens?: Array|Iterable; + + /** + * The maximum number of times this operation should be retried. If set, this value will override the `maxRetries` configuration set on the client for this command. + */ + $maxRetries?: number; + + /** + * An object that may be queried to determine if the underlying operation has been aborted. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal + */ + $abortSignal?: __aws_sdk_types.AbortSignal; + + /** + * Per-request HTTP configuration options. If set, any options specified will override the corresponding HTTP option set on the client for this command. + */ + $httpOptions?: NodeHttpOptions|BrowserHttpOptions; +} \ No newline at end of file diff --git a/modules/kms-keyring/src/kms_types/EncryptOutput.ts b/modules/kms-keyring/src/kms_types/EncryptOutput.ts new file mode 100644 index 000000000..8bb46dfe7 --- /dev/null +++ b/modules/kms-keyring/src/kms_types/EncryptOutput.ts @@ -0,0 +1,21 @@ +import * as __aws_sdk_types from '@aws-sdk/types'; + +/** + * EncryptOutput shape + */ +export interface EncryptOutput { + /** + *

The encrypted plaintext. When you use the HTTP API or the AWS CLI, the value is Base64-encoded. Otherwise, it is not encoded.

+ */ + CiphertextBlob?: Uint8Array; + + /** + *

The ID of the key used during encryption.

+ */ + KeyId?: string; + + /** + * Metadata about the response received, including the HTTP status code, HTTP headers, and any request identifiers recognized by the SDK. + */ + $metadata: __aws_sdk_types.ResponseMetadata; +} diff --git a/modules/kms-keyring/src/kms_types/GenerateDataKeyInput.ts b/modules/kms-keyring/src/kms_types/GenerateDataKeyInput.ts new file mode 100644 index 000000000..f82ecd018 --- /dev/null +++ b/modules/kms-keyring/src/kms_types/GenerateDataKeyInput.ts @@ -0,0 +1,49 @@ +import { NodeHttpOptions, BrowserHttpOptions } from '@aws-sdk/types'; +import * as __aws_sdk_types from '@aws-sdk/types'; + +/** + * GenerateDataKeyInput shape + */ +export interface GenerateDataKeyInput { + /** + *

The identifier of the CMK under which to generate and encrypt the data encryption key.

To specify a CMK, use its key ID, Amazon Resource Name (ARN), alias name, or alias ARN. When using an alias name, prefix it with "alias/". To specify a CMK in a different AWS account, you must use the key ARN or alias ARN.

For example:

  • Key ID: 1234abcd-12ab-34cd-56ef-1234567890ab

  • Key ARN: arn:aws:kms:us-east-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab

  • Alias name: alias/ExampleAlias

  • Alias ARN: arn:aws:kms:us-east-2:111122223333:alias/ExampleAlias

To get the key ID and key ARN for a CMK, use ListKeys or DescribeKey. To get the alias name and alias ARN, use ListAliases.

+ */ + KeyId: string; + + /** + *

A set of key-value pairs that represents additional authenticated data.

For more information, see Encryption Context in the AWS Key Management Service Developer Guide.

+ */ + EncryptionContext?: {[key: string]: string}|Iterable<[string, string]>; + + /** + *

The length of the data encryption key in bytes. For example, use the value 64 to generate a 512-bit data key (64 bytes is 512 bits). For common key lengths (128-bit and 256-bit symmetric keys), we recommend that you use the KeySpec field instead of this one.

+ */ + NumberOfBytes?: number; + + /** + *

The length of the data encryption key. Use AES_128 to generate a 128-bit symmetric key, or AES_256 to generate a 256-bit symmetric key.

+ */ + KeySpec?: 'AES_256'|'AES_128'|string; + + /** + *

A list of grant tokens.

For more information, see Grant Tokens in the AWS Key Management Service Developer Guide.

+ */ + GrantTokens?: Array|Iterable; + + /** + * The maximum number of times this operation should be retried. If set, this value will override the `maxRetries` configuration set on the client for this command. + */ + $maxRetries?: number; + + /** + * An object that may be queried to determine if the underlying operation has been aborted. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal + */ + $abortSignal?: __aws_sdk_types.AbortSignal; + + /** + * Per-request HTTP configuration options. If set, any options specified will override the corresponding HTTP option set on the client for this command. + */ + $httpOptions?: NodeHttpOptions|BrowserHttpOptions; +} \ No newline at end of file diff --git a/modules/kms-keyring/src/kms_types/GenerateDataKeyOutput.ts b/modules/kms-keyring/src/kms_types/GenerateDataKeyOutput.ts new file mode 100644 index 000000000..d6c939248 --- /dev/null +++ b/modules/kms-keyring/src/kms_types/GenerateDataKeyOutput.ts @@ -0,0 +1,26 @@ +import * as __aws_sdk_types from '@aws-sdk/types'; + +/** + * GenerateDataKeyOutput shape + */ +export interface GenerateDataKeyOutput { + /** + *

The encrypted data encryption key. When you use the HTTP API or the AWS CLI, the value is Base64-encoded. Otherwise, it is not encoded.

+ */ + CiphertextBlob?: Uint8Array; + + /** + *

The data encryption key. When you use the HTTP API or the AWS CLI, the value is Base64-encoded. Otherwise, it is not encoded. Use this data key for local encryption and decryption, then remove it from memory as soon as possible.

+ */ + Plaintext?: Uint8Array; + + /** + *

The identifier of the CMK under which the data encryption key was generated and encrypted.

+ */ + KeyId?: string; + + /** + * Metadata about the response received, including the HTTP status code, HTTP headers, and any request identifiers recognized by the SDK. + */ + $metadata: __aws_sdk_types.ResponseMetadata; +} diff --git a/modules/kms-keyring/src/kms_types/KMS.ts b/modules/kms-keyring/src/kms_types/KMS.ts new file mode 100644 index 000000000..5ccd23a34 --- /dev/null +++ b/modules/kms-keyring/src/kms_types/KMS.ts @@ -0,0 +1,60 @@ + +import {DecryptInput} from './DecryptInput'; +import {DecryptOutput} from './DecryptOutput'; + +import {EncryptInput} from './EncryptInput'; +import {EncryptOutput} from './EncryptOutput'; + +import {GenerateDataKeyInput} from './GenerateDataKeyInput'; +import {GenerateDataKeyOutput} from './GenerateDataKeyOutput'; + +export interface KMS { + /** + *

Decrypts ciphertext. Ciphertext is plaintext that has been previously encrypted by using any of the following operations:

Whenever possible, use key policies to give users permission to call the Decrypt operation on the CMK, instead of IAM policies. Otherwise, you might create an IAM user policy that gives the user Decrypt permission on all CMKs. This user could decrypt ciphertext that was encrypted by CMKs in other accounts if the key policy for the cross-account CMK permits it. If you must use an IAM policy for Decrypt permissions, limit the user to particular CMKs or particular trusted accounts.

The result of this operation varies with the key state of the CMK. For details, see How Key State Affects Use of a Customer Master Key in the AWS Key Management Service Developer Guide.

+ * + * This operation may fail with one of the following errors: + * - {NotFoundException}

The request was rejected because the specified entity or resource could not be found.

+ * - {DisabledException}

The request was rejected because the specified CMK is not enabled.

+ * - {InvalidCiphertextException}

The request was rejected because the specified ciphertext, or additional authenticated data incorporated into the ciphertext, such as the encryption context, is corrupted, missing, or otherwise invalid.

+ * - {KeyUnavailableException}

The request was rejected because the specified CMK was not available. The request can be retried.

+ * - {DependencyTimeoutException}

The system timed out while trying to fulfill the request. The request can be retried.

+ * - {InvalidGrantTokenException}

The request was rejected because the specified grant token is not valid.

+ * - {KMSInternalException}

The request was rejected because an internal exception occurred. The request can be retried.

+ * - {KMSInvalidStateException}

The request was rejected because the state of the specified resource is not valid for this request.

For more information about how key state affects the use of a CMK, see How Key State Affects Use of a Customer Master Key in the AWS Key Management Service Developer Guide.

+ * - {Error} An error originating from the SDK or customizations rather than the service + */ + decrypt(args: DecryptInput): Promise; + + /** + *

Encrypts plaintext into ciphertext by using a customer master key (CMK). The Encrypt operation has two primary use cases:

  • You can encrypt up to 4 kilobytes (4096 bytes) of arbitrary data such as an RSA key, a database password, or other sensitive information.

  • You can use the Encrypt operation to move encrypted data from one AWS region to another. In the first region, generate a data key and use the plaintext key to encrypt the data. Then, in the new region, call the Encrypt method on same plaintext data key. Now, you can safely move the encrypted data and encrypted data key to the new region, and decrypt in the new region when necessary.

You don't need use this operation to encrypt a data key within a region. The GenerateDataKey and GenerateDataKeyWithoutPlaintext operations return an encrypted data key.

Also, you don't need to use this operation to encrypt data in your application. You can use the plaintext and encrypted data keys that the GenerateDataKey operation returns.

The result of this operation varies with the key state of the CMK. For details, see How Key State Affects Use of a Customer Master Key in the AWS Key Management Service Developer Guide.

To perform this operation on a CMK in a different AWS account, specify the key ARN or alias ARN in the value of the KeyId parameter.

+ * + * This operation may fail with one of the following errors: + * - {NotFoundException}

The request was rejected because the specified entity or resource could not be found.

+ * - {DisabledException}

The request was rejected because the specified CMK is not enabled.

+ * - {KeyUnavailableException}

The request was rejected because the specified CMK was not available. The request can be retried.

+ * - {DependencyTimeoutException}

The system timed out while trying to fulfill the request. The request can be retried.

+ * - {InvalidKeyUsageException}

The request was rejected because the specified KeySpec value is not valid.

+ * - {InvalidGrantTokenException}

The request was rejected because the specified grant token is not valid.

+ * - {KMSInternalException}

The request was rejected because an internal exception occurred. The request can be retried.

+ * - {KMSInvalidStateException}

The request was rejected because the state of the specified resource is not valid for this request.

For more information about how key state affects the use of a CMK, see How Key State Affects Use of a Customer Master Key in the AWS Key Management Service Developer Guide.

+ * - {Error} An error originating from the SDK or customizations rather than the service + */ + encrypt(args: EncryptInput): Promise; + + /** + *

Returns a data encryption key that you can use in your application to encrypt data locally.

You must specify the customer master key (CMK) under which to generate the data key. You must also specify the length of the data key using either the KeySpec or NumberOfBytes field. You must specify one field or the other, but not both. For common key lengths (128-bit and 256-bit symmetric keys), we recommend that you use KeySpec. To perform this operation on a CMK in a different AWS account, specify the key ARN or alias ARN in the value of the KeyId parameter.

This operation returns a plaintext copy of the data key in the Plaintext field of the response, and an encrypted copy of the data key in the CiphertextBlob field. The data key is encrypted under the CMK specified in the KeyId field of the request.

We recommend that you use the following pattern to encrypt data locally in your application:

  1. Use this operation (GenerateDataKey) to get a data encryption key.

  2. Use the plaintext data encryption key (returned in the Plaintext field of the response) to encrypt data locally, then erase the plaintext data key from memory.

  3. Store the encrypted data key (returned in the CiphertextBlob field of the response) alongside the locally encrypted data.

To decrypt data locally:

  1. Use the Decrypt operation to decrypt the encrypted data key into a plaintext copy of the data key.

  2. Use the plaintext data key to decrypt data locally, then erase the plaintext data key from memory.

To return only an encrypted copy of the data key, use GenerateDataKeyWithoutPlaintext. To return a random byte string that is cryptographically secure, use GenerateRandom.

If you use the optional EncryptionContext field, you must store at least enough information to be able to reconstruct the full encryption context when you later send the ciphertext to the Decrypt operation. It is a good practice to choose an encryption context that you can reconstruct on the fly to better secure the ciphertext. For more information, see Encryption Context in the AWS Key Management Service Developer Guide.

The result of this operation varies with the key state of the CMK. For details, see How Key State Affects Use of a Customer Master Key in the AWS Key Management Service Developer Guide.

+ * + * This operation may fail with one of the following errors: + * - {NotFoundException}

The request was rejected because the specified entity or resource could not be found.

+ * - {DisabledException}

The request was rejected because the specified CMK is not enabled.

+ * - {KeyUnavailableException}

The request was rejected because the specified CMK was not available. The request can be retried.

+ * - {DependencyTimeoutException}

The system timed out while trying to fulfill the request. The request can be retried.

+ * - {InvalidKeyUsageException}

The request was rejected because the specified KeySpec value is not valid.

+ * - {InvalidGrantTokenException}

The request was rejected because the specified grant token is not valid.

+ * - {KMSInternalException}

The request was rejected because an internal exception occurred. The request can be retried.

+ * - {KMSInvalidStateException}

The request was rejected because the state of the specified resource is not valid for this request.

For more information about how key state affects the use of a CMK, see How Key State Affects Use of a Customer Master Key in the AWS Key Management Service Developer Guide.

+ * - {Error} An error originating from the SDK or customizations rather than the service + */ + generateDataKey(args: GenerateDataKeyInput): Promise; + +} diff --git a/modules/kms-keyring/src/kms_types/KMSConfiguration.ts b/modules/kms-keyring/src/kms_types/KMSConfiguration.ts new file mode 100644 index 000000000..1127b697c --- /dev/null +++ b/modules/kms-keyring/src/kms_types/KMSConfiguration.ts @@ -0,0 +1,110 @@ +import * as __aws_sdk_types from '@aws-sdk/types'; + +export interface KMSConfiguration { + /** + * The function that will be used to convert a base64-encoded string to a byte array + */ + base64Decoder?: __aws_sdk_types.Decoder; + + /** + * The function that will be used to convert binary data to a base64-encoded string + */ + base64Encoder?: __aws_sdk_types.Encoder; + + /** + * The credentials used to sign requests. + * + * If no static credentials are supplied, the SDK will attempt to credentials from known environment variables, from shared configuration and credentials files, and from the EC2 Instance Metadata Service, in that order. + */ + credentials?: __aws_sdk_types.Credentials|__aws_sdk_types.Provider<__aws_sdk_types.Credentials>; + + /** + * A function that determines how long (in milliseconds) the SDK should wait before retrying a request + */ + delayDecider?: __aws_sdk_types.DelayDecider; + + /** + * The fully qualified endpoint of the webservice. This is only required when using a custom endpoint (for example, when using a local version of S3). + */ + endpoint?: string|__aws_sdk_types.HttpEndpoint|__aws_sdk_types.Provider<__aws_sdk_types.HttpEndpoint>; + + /** + * The endpoint provider to call if no endpoint is provided + */ + endpointProvider?: any; + + // /** + // * The handler to use as the core of the client's middleware stack + // */ + // handler?: __aws_sdk_types.Terminalware; + + // /** + // * The HTTP handler to use + // */ + // httpHandler?: __aws_sdk_types.HttpHandler<_stream.Readable>; + + /** + * The maximum number of redirects to follow for a service request. Set to `0` to disable retries. + */ + maxRedirects?: number; + + /** + * The maximum number of times requests that encounter potentially transient failures should be retried + */ + maxRetries?: number; + + /** + * The configuration profile to use. + */ + profile?: string; + + /** + * The AWS region to which this client will send requests + */ + region?: string|__aws_sdk_types.Provider; + + /** + * A function that determines whether an error is retryable + */ + retryDecider?: __aws_sdk_types.RetryDecider; + + /** + * A constructor for a class implementing the @aws-sdk/types.Hash interface that computes the SHA-256 HMAC or checksum of a string or binary buffer + */ + sha256?: __aws_sdk_types.HashConstructor; + + /** + * The signer to use when signing requests. + */ + signer?: __aws_sdk_types.RequestSigner; + + /** + * The service name with which to sign requests. + */ + signingName?: string; + + /** + * Whether SSL is enabled for requests. + */ + sslEnabled?: boolean; + + // /** + // * A function that converts a stream into an array of bytes. + // */ + // streamCollector?: __aws_sdk_types.StreamCollector<_stream.Readable>; + + /** + * The function that will be used to convert strings into HTTP endpoints + */ + urlParser?: __aws_sdk_types.UrlParser; + + /** + * The function that will be used to convert a UTF8-encoded string to a byte array + */ + utf8Decoder?: __aws_sdk_types.Decoder; + + /** + * The function that will be used to convert binary data to a UTF-8 encoded string + */ + utf8Encoder?: __aws_sdk_types.Encoder; +} diff --git a/modules/kms-keyring/src/kms_types/Readme.md b/modules/kms-keyring/src/kms_types/Readme.md new file mode 100644 index 000000000..c50daf5c6 --- /dev/null +++ b/modules/kms-keyring/src/kms_types/Readme.md @@ -0,0 +1,10 @@ +At this time the AWS SDK for JavaScript v3 only export the +client for Node.js or browser. +Since the types are the same and are _only_ exported in the packages +themselves, to support both versions I can either include both +packages _or_ copy the types here. I have chose to copy the types +for 2 reasons. + +1. I use an exceedingly limited set of the entire KMS API +1. The KMS API has been stable and unchanged since 2014-11-01 +1. Given that the aws js sdk v3 was split up, merging them back together seem counter productive. diff --git a/modules/kms-keyring/src/region_from_kms_key_arn.ts b/modules/kms-keyring/src/region_from_kms_key_arn.ts new file mode 100644 index 000000000..4c14a901e --- /dev/null +++ b/modules/kms-keyring/src/region_from_kms_key_arn.ts @@ -0,0 +1,40 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use + * this file except in compliance with the License. A copy of the License is + * located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { needs } from '@aws-crypto/material-management' + +export function regionFromKmsKeyArn (kmsKeyArn: string): string { + /* Precondition: A KMS key arn must be a string. */ + needs(typeof kmsKeyArn === 'string', 'KMS key arn must be a string.') + + /* See: https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html#arn-syntax-kms + * arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012 + * arn:aws:kms:us-east-1:123456789012:alias/example-alias + */ + const [arnLiteral, partition, service, region] = kmsKeyArn.split(':') + + /* Postcondition: The ARN must be well formed. + * The arn and kms section have defined values, + * but the aws section does not. + */ + needs( + arnLiteral === 'arn' && + partition && + service === 'kms' && + region, + 'Malformed arn.') + + return region +} diff --git a/modules/kms-keyring/test/helpers.test.ts b/modules/kms-keyring/test/helpers.test.ts new file mode 100644 index 000000000..54a26dccd --- /dev/null +++ b/modules/kms-keyring/test/helpers.test.ts @@ -0,0 +1,272 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use + * this file except in compliance with the License. A copy of the License is + * located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* eslint-env mocha */ + +import { expect } from 'chai' +import 'mocha' +import { generateDataKey, encrypt, decrypt, kms2EncryptedDataKey } from '../src/helpers' +import { EncryptedDataKey } from '@aws-crypto/material-management' +import { GenerateDataKeyInput } from '../src/kms_types/GenerateDataKeyInput' // eslint-disable-line no-unused-vars +import { EncryptInput } from '../src/kms_types/EncryptInput' // eslint-disable-line no-unused-vars +import { DecryptInput } from '../src/kms_types/DecryptInput' // eslint-disable-line no-unused-vars + +describe('kms2EncryptedDataKey', () => { + it('return an EncryptedDataKey', () => { + const response = { + KeyId: 'asdf', + CiphertextBlob: new Uint8Array(5), + $metadata: {} as any + } + const test = kms2EncryptedDataKey(response) + expect(test).instanceOf(EncryptedDataKey) + expect(test.providerId).to.equal('aws-kms') + expect(test.providerInfo).to.equal('asdf') + expect(test.encryptedDataKey.byteLength).to.equal(5) + }) +}) + +describe('generateDataKey', () => { + it('return', async () => { + const KeyId = 'arn:aws:kms:us-east-1:123456789012:alias/example-alias' + const GrantTokens = 'grantToken' + const NumberOfBytes = 128 + const EncryptionContext = { some: 'context' } + + const clientProvider: any = (region: string) => { + expect(region).to.equal('us-east-1') + return { generateDataKey } + function generateDataKey (input: GenerateDataKeyInput) { + expect(input.KeyId).to.equal(KeyId) + expect(input.GrantTokens).to.equal(GrantTokens) + expect(input.NumberOfBytes).to.equal(NumberOfBytes) + expect(input.EncryptionContext).to.equal(EncryptionContext) + return { + Plaintext: 'Plaintext', + KeyId: 'KeyId', + CiphertextBlob: 'CiphertextBlob' + } + } + } + + const test = await generateDataKey(clientProvider, NumberOfBytes, KeyId, EncryptionContext, GrantTokens) + if (!test) throw new Error('never') + expect(test.Plaintext).to.equal('Plaintext') + expect(test.KeyId).to.equal('KeyId') + expect(test.CiphertextBlob).to.equal('CiphertextBlob') + }) + + it('Check for early return (Postcondition): Client region was not provided.', async () => { + const KeyId = 'arn:aws:kms:us-east-1:123456789012:alias/example-alias' + const GrantTokens = 'grantToken' + const NumberOfBytes = 128 + const EncryptionContext = { some: 'context' } + + const clientProvider: any = () => { + return false + } + + const test = await generateDataKey(clientProvider, NumberOfBytes, KeyId, EncryptionContext, GrantTokens) + expect(test).to.equal(false) + }) + + it('Postcondition: KMS must return serializable generate data key.', async () => { + const KeyId = 'arn:aws:kms:us-east-1:123456789012:alias/example-alias' + const GrantTokens = 'grantToken' + const NumberOfBytes = 128 + const EncryptionContext = { some: 'context' } + + const clientProvider: any = () => { + return { generateDataKey } + function generateDataKey () { + return {} + } + } + + try { + await generateDataKey(clientProvider, NumberOfBytes, KeyId, EncryptionContext, GrantTokens) + } catch { + return + } + throw new Error('never') + }) +}) + +describe('encrypt', () => { + it('return', async () => { + const KeyId = 'arn:aws:kms:us-east-1:123456789012:alias/example-alias' + const GrantTokens = 'grantToken' + const Plaintext = new Uint8Array(5) + const EncryptionContext = { some: 'context' } + + const clientProvider: any = (region: string) => { + expect(region).to.equal('us-east-1') + return { encrypt } + function encrypt (input: EncryptInput) { + expect(input.KeyId).to.equal(KeyId) + expect(input.GrantTokens).to.equal(GrantTokens) + expect(input.Plaintext).to.equal(Plaintext) + expect(input.EncryptionContext).to.equal(EncryptionContext) + return { + KeyId: 'KeyId', + CiphertextBlob: 'CiphertextBlob' + } + } + } + + const test = await encrypt(clientProvider, Plaintext, KeyId, EncryptionContext, GrantTokens) + if (!test) throw new Error('never') + expect(test.KeyId).to.equal('KeyId') + expect(test.CiphertextBlob).to.equal('CiphertextBlob') + }) + + it('Check for early return (Postcondition): Client region was not provided.', async () => { + const KeyId = 'arn:aws:kms:us-east-1:123456789012:alias/example-alias' + const GrantTokens = 'grantToken' + const Plaintext = new Uint8Array(5) + const EncryptionContext = { some: 'context' } + + const clientProvider: any = () => { + return false + } + + const test = await encrypt(clientProvider, Plaintext, KeyId, EncryptionContext, GrantTokens) + expect(test).to.equal(false) + }) + + it('Postcondition: KMS must return serializable encrypted data key.', async () => { + const KeyId = 'arn:aws:kms:us-east-1:123456789012:alias/example-alias' + const GrantTokens = 'grantToken' + const Plaintext = new Uint8Array(5) + const EncryptionContext = { some: 'context' } + + const clientProvider: any = () => { + return { encrypt } + function encrypt () { + return {} + } + } + + try { + await encrypt(clientProvider, Plaintext, KeyId, EncryptionContext, GrantTokens) + } catch { + return + } + throw new Error('never') + }) +}) + +describe('decrypt', () => { + it('return', async () => { + const GrantTokens = 'grantToken' + const KeyId = 'arn:aws:kms:us-east-1:123456789012:alias/example-alias' + const edk = new EncryptedDataKey({ + providerId: 'aws-kms', + providerInfo: KeyId, + encryptedDataKey: new Uint8Array(5) + }) + const EncryptionContext = { some: 'context' } + + const clientProvider: any = (region: string) => { + expect(region).to.equal('us-east-1') + return { decrypt } + function decrypt (input: DecryptInput) { + expect(input.GrantTokens).to.equal(GrantTokens) + expect(input.CiphertextBlob).lengthOf(5) + expect(input.EncryptionContext).to.equal(EncryptionContext) + return { + KeyId: 'KeyId', + Plaintext: 'Plaintext' + } + } + } + + const test = await decrypt(clientProvider, edk, EncryptionContext, GrantTokens) + if (!test) throw new Error('never') + expect(test.KeyId).to.equal('KeyId') + expect(test.Plaintext).to.equal('Plaintext') + }) + + it('Precondition: The EDK must be a KMS edk.', async () => { + const GrantTokens = 'grantToken' + const KeyId = 'arn:aws:kms:us-east-1:123456789012:alias/example-alias' + const edk = new EncryptedDataKey({ + providerId: 'NOTaws-kms', + providerInfo: KeyId, + encryptedDataKey: new Uint8Array(5) + }) + const EncryptionContext = { some: 'context' } + + const clientProvider: any = () => { + return { decrypt } + function decrypt () { + return { + KeyId: 'KeyId', + Plaintext: 'Plaintext' + } + } + } + + try { + await decrypt(clientProvider, edk, EncryptionContext, GrantTokens) + } catch { + return + } + throw new Error('never') + }) + + it('Check for early return (Postcondition): Client region was not provided.', async () => { + const GrantTokens = 'grantToken' + const KeyId = 'arn:aws:kms:us-east-1:123456789012:alias/example-alias' + const edk = new EncryptedDataKey({ + providerId: 'aws-kms', + providerInfo: KeyId, + encryptedDataKey: new Uint8Array(5) + }) + const EncryptionContext = { some: 'context' } + + const clientProvider: any = () => { + return false + } + + const test = await decrypt(clientProvider, edk, EncryptionContext, GrantTokens) + expect(test).to.equal(false) + }) + + it('Postcondition: KMS must return usable decrypted key.', async () => { + const GrantTokens = 'grantToken' + const KeyId = 'arn:aws:kms:us-east-1:123456789012:alias/example-alias' + const edk = new EncryptedDataKey({ + providerId: 'aws-kms', + providerInfo: KeyId, + encryptedDataKey: new Uint8Array(5) + }) + const EncryptionContext = { some: 'context' } + + const clientProvider: any = () => { + return { decrypt } + function decrypt () { + return {} + } + } + + try { + await decrypt(clientProvider, edk, EncryptionContext, GrantTokens) + } catch { + return + } + throw new Error('never') + }) +}) diff --git a/modules/kms-keyring/test/kms_client_supplier.test.ts b/modules/kms-keyring/test/kms_client_supplier.test.ts new file mode 100644 index 000000000..c3ee9b2c3 --- /dev/null +++ b/modules/kms-keyring/test/kms_client_supplier.test.ts @@ -0,0 +1,190 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use + * this file except in compliance with the License. A copy of the License is + * located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* eslint-env mocha */ + +import { expect } from 'chai' +import 'mocha' +import { getClient, limitRegions, excludeRegions, cacheClients } from '../src/kms_client_supplier' +import { KMSConfiguration } from '../src/kms_types/KMSConfiguration' // eslint-disable-line no-unused-vars + +describe('getClient', () => { + it('return a client', () => { + const region = 'us-west-2' + let assertCount = 0 + const TestKMS: any = class { + constructor (config: KMSConfiguration) { + expect(config.region).to.equal(region) + assertCount++ + } + } + const getKmsClient = getClient(TestKMS) + const test = getKmsClient(region) + expect(test).instanceOf(TestKMS) + expect(assertCount).to.equal(1) + }) + + it('Precondition: region be a string.', () => { + let assertCount = 0 + const TestKMS: any = class { + constructor () { + assertCount++ + } + } + const getKmsClient = getClient(TestKMS) + expect(() => getKmsClient('')).to.throw() + expect(() => getKmsClient({} as any)).to.throw() + expect(assertCount).to.equal(0) + }) +}) + +describe('limitRegions', () => { + it('return a client', () => { + const region = 'us-west-2' + let assertCount = 0 + const TestKMS: any = class { + constructor (config: KMSConfiguration) { + expect(config.region).to.equal(region) + assertCount++ + } + } + const getKmsClient = getClient(TestKMS) + const limitKmsClient = limitRegions(['us-west-2'], getKmsClient) + const test = limitKmsClient(region) + expect(test).instanceOf(TestKMS) + expect(assertCount).to.equal(1) + }) + + it('limits by region', () => { + const region = 'us-west-2' + let assertCount = 0 + const TestKMS: any = class { + constructor (config: KMSConfiguration) { + expect(config.region).to.equal(region) + assertCount++ + } + } + const getKmsClient = getClient(TestKMS) + const limitKmsClient = limitRegions(['us-east-2'], getKmsClient) + const test = limitKmsClient(region) + expect(test).to.equal(false) + expect(assertCount).to.equal(0) + }) + + it('Precondition: region be a string.', () => { + expect(() => limitRegions(['us-east-2', ''], (() => {}) as any)).to.throw() + expect(() => limitRegions(['us-east-2', {}] as any, (() => {}) as any)).to.throw() + }) +}) + +describe('excludeRegions', () => { + it('exclude client', () => { + const region = 'us-west-2' + let assertCount = 0 + const TestKMS: any = class { + constructor (config: KMSConfiguration) { + expect(config.region).to.equal(region) + assertCount++ + } + } + const getKmsClient = getClient(TestKMS) + const excludeKmsClient = excludeRegions(['us-west-2'], getKmsClient) + const test = excludeKmsClient(region) + expect(test).to.equal(false) + expect(assertCount).to.equal(0) + }) + + it('return a client', () => { + const region = 'us-west-2' + let assertCount = 0 + const TestKMS: any = class { + constructor (config: KMSConfiguration) { + expect(config.region).to.equal(region) + assertCount++ + } + } + const getKmsClient = getClient(TestKMS) + const excludeKmsClient = excludeRegions(['us-east-2'], getKmsClient) + const test = excludeKmsClient(region) + expect(test).instanceOf(TestKMS) + expect(assertCount).to.equal(1) + }) + + it('Precondition: region be a string.', () => { + expect(() => excludeRegions(['us-east-2', ''], (() => {}) as any)).to.throw() + expect(() => excludeRegions(['us-east-2', {}] as any, (() => {}) as any)).to.throw() + }) +}) + +describe('cacheClients', () => { + it('return a client', () => { + const region = 'us-west-2' + let assertCount = 0 + const TestKMS: any = class { + constructor (config: KMSConfiguration) { + expect(config.region).to.equal(region) + assertCount++ + } + } + const getKmsClient = cacheClients(getClient(TestKMS)) + const test = getKmsClient(region) + expect(test).instanceOf(TestKMS) + expect(assertCount).to.equal(1) + }) + + it('does not cache the client until KMS has been contacted', () => { + const region = 'us-west-2' + let assertCount = 0 + const TestKMS: any = class { + constructor (config: KMSConfiguration) { + expect(config.region).to.equal(region) + assertCount++ + } + } + const getKmsClient = cacheClients(getClient(TestKMS)) + const test = getKmsClient(region) + expect(test).instanceOf(TestKMS) + expect(assertCount).to.equal(1) + + const test2 = getKmsClient(region) + expect(test === test2).to.equal(false) + expect(assertCount).to.equal(2) + }) + + it('cache the client after KMS has been contacted', async () => { + const region = 'us-west-2' + let assertCount = 0 + const TestKMS: any = class { + constructor (config: KMSConfiguration) { + expect(config.region).to.equal(region) + assertCount++ + } + async decrypt () { + + } + } + const getKmsClient = cacheClients(getClient(TestKMS)) + const test = getKmsClient(region) + if (!test) throw new Error('never') + expect(test).instanceOf(TestKMS) + expect(assertCount).to.equal(1) + + await test.decrypt({} as any) + + const test2 = getKmsClient(region) + expect(test === test2).to.equal(true) + expect(assertCount).to.equal(1) + }) +}) diff --git a/modules/kms-keyring/test/kms_keyring.constructor.test.ts b/modules/kms-keyring/test/kms_keyring.constructor.test.ts new file mode 100644 index 000000000..e9edb8a3e --- /dev/null +++ b/modules/kms-keyring/test/kms_keyring.constructor.test.ts @@ -0,0 +1,94 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use + * this file except in compliance with the License. A copy of the License is + * located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* eslint-env mocha */ + +import { expect } from 'chai' +import 'mocha' +import { + KmsKeyringClass, + KeyRingConstructible // eslint-disable-line no-unused-vars +} from '../src/kms_keyring' +import { NodeAlgorithmSuite, Keyring } from '@aws-crypto/material-management' // eslint-disable-line no-unused-vars + +describe('KmsKeyring: constructor', () => { + it('set properties', () => { + const clientProvider: any = () => {} + const generatorKeyId = 'arn:aws:kms:us-east-1:123456789012:alias/example-alias' + const keyIds = ['arn:aws:kms:us-east-1:123456789012:alias/example-alias'] + const grantTokens = 'grant' + + class TestKmsKeyring extends KmsKeyringClass(Keyring as KeyRingConstructible) {} + + const test = new TestKmsKeyring({ clientProvider, generatorKeyId, keyIds, grantTokens }) + expect(test.clientProvider).to.equal(clientProvider) + expect(test.generatorKeyId).to.equal(generatorKeyId) + expect(test.keyIds).to.deep.equal(keyIds) + expect(test.grantTokens).to.equal(grantTokens) + expect(test.isDiscovery).to.equal(false) + }) + + it('set properties for discovery keyring', () => { + const clientProvider: any = () => {} + const discovery = true + + class TestKmsKeyring extends KmsKeyringClass(Keyring as KeyRingConstructible) {} + + const test = new TestKmsKeyring({ clientProvider, discovery }) + expect(test.clientProvider).to.equal(clientProvider) + expect(test.generatorKeyId).to.equal(undefined) + expect(test.keyIds).to.deep.equal([]) + expect(test.grantTokens).to.equal(undefined) + expect(test.isDiscovery).to.equal(true) + }) + + it('Precondition: A noop KmsKeyring is not allowed. You must explicitly set discovery or keyIds.', () => { + class TestKmsKeyring extends KmsKeyringClass(Keyring as KeyRingConstructible) {} + const clientProvider: any = () => {} + expect(() => new TestKmsKeyring({ clientProvider })).to.throw() + + const generatorKeyId = 'arn:aws:kms:us-east-1:123456789012:alias/example-alias' + const keyIds = ['arn:aws:kms:us-east-1:123456789012:alias/example-alias'] + const discovery = true + expect(() => new TestKmsKeyring({ clientProvider, generatorKeyId, keyIds, discovery })).to.throw() + }) + + it('Precondition: All KMS key arns must be valid.', () => { + const clientProvider: any = () => {} + class TestKmsKeyring extends KmsKeyringClass(Keyring as KeyRingConstructible) {} + + expect(() => new TestKmsKeyring({ + clientProvider, + generatorKeyId: 'Not arn' + })).to.throw() + + expect(() => new TestKmsKeyring({ + clientProvider, + keyIds: ['Not arn'] + })).to.throw() + + expect(() => new TestKmsKeyring({ + clientProvider, + keyIds: ['arn:aws:kms:us-east-1:123456789012:alias/example-alias', 'Not arn'] + })).to.throw() + }) + + it('Precondition: clientProvider needs to be a callable function.', () => { + class TestKmsKeyring extends KmsKeyringClass(Keyring as KeyRingConstructible) {} + const clientProvider: any = 'not function' + const discovery = true + expect(() => new TestKmsKeyring({ clientProvider, discovery })).to.throw() + }) +}) diff --git a/modules/kms-keyring/test/kms_keyring.ondecrypt.test.ts b/modules/kms-keyring/test/kms_keyring.ondecrypt.test.ts new file mode 100644 index 000000000..5066ac252 --- /dev/null +++ b/modules/kms-keyring/test/kms_keyring.ondecrypt.test.ts @@ -0,0 +1,173 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use + * this file except in compliance with the License. A copy of the License is + * located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* eslint-env mocha */ + +import chai from 'chai' +import chaiAsPromised from 'chai-as-promised' +import 'mocha' +import { + KmsKeyringClass, + KeyRingConstructible // eslint-disable-line no-unused-vars +} from '../src/kms_keyring' +import { + NodeAlgorithmSuite, + AlgorithmSuiteIdentifier, + KeyringTraceFlag, + NodeDecryptionMaterial, + EncryptedDataKey, + Keyring +} from '@aws-crypto/material-management' +import { DecryptInput } from '../src/kms_types/DecryptInput' // eslint-disable-line no-unused-vars +chai.use(chaiAsPromised) +const { expect } = chai + +describe('KmsKeyring: _onDecrypt', + () => { + it('returns material', async () => { + const generatorKeyId = 'arn:aws:kms:us-east-1:123456789012:alias/example-alias' + const encryptKmsKey = 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012' + const keyIds = [encryptKmsKey] + const context = { some: 'context' } + const grantTokens = 'grant' + const suite = new NodeAlgorithmSuite(AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16) + + const clientProvider: any = () => { + return { decrypt } + function decrypt ({ CiphertextBlob, EncryptionContext, GrantTokens }: DecryptInput) { + expect(EncryptionContext === context).to.equal(true) + expect(GrantTokens).to.equal(grantTokens) + return { + Plaintext: new Uint8Array(suite.keyLengthBytes), + KeyId: Buffer.from(CiphertextBlob).toString('utf8') + } + } + } + class TestKmsKeyring extends KmsKeyringClass(Keyring as KeyRingConstructible) {} + + const testKeyring = new TestKmsKeyring({ + clientProvider, + generatorKeyId, + keyIds, + grantTokens + }) + + const edk = new EncryptedDataKey({ + providerId: 'aws-kms', + providerInfo: generatorKeyId, + encryptedDataKey: Buffer.from(generatorKeyId) + }) + + const material = await testKeyring.onDecrypt( + new NodeDecryptionMaterial(suite), + [edk], + context + ) + + expect(material.hasUnencryptedDataKey).to.equal(true) + + expect(material.keyringTrace).to.have.lengthOf(1) + const [traceDecrypt] = material.keyringTrace + expect(traceDecrypt.keyNamespace).to.equal('aws-kms') + expect(traceDecrypt.keyName).to.equal(generatorKeyId) + expect(traceDecrypt.flags & KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY).to.equal(KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY) + expect(traceDecrypt.flags & KeyringTraceFlag.WRAPPING_KEY_VERIFIED_ENC_CTX).to.equal(KeyringTraceFlag.WRAPPING_KEY_VERIFIED_ENC_CTX) + }) + + it('discovery keyring should return material', async () => { + const generatorKeyId = 'arn:aws:kms:us-east-1:123456789012:alias/example-alias' + const context = { some: 'context' } + const grantTokens = 'grant' + const discovery = true + const suite = new NodeAlgorithmSuite(AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16) + + const clientProvider: any = () => { + return { decrypt } + function decrypt ({ CiphertextBlob, EncryptionContext, GrantTokens }: DecryptInput) { + expect(EncryptionContext === context).to.equal(true) + expect(GrantTokens).to.equal(grantTokens) + return { + Plaintext: new Uint8Array(suite.keyLengthBytes), + KeyId: Buffer.from(CiphertextBlob).toString('utf8') + } + } + } + class TestKmsKeyring extends KmsKeyringClass(Keyring as KeyRingConstructible) {} + + const testKeyring = new TestKmsKeyring({ + clientProvider, + grantTokens, + discovery + }) + + const edk = new EncryptedDataKey({ + providerId: 'aws-kms', + providerInfo: generatorKeyId, + encryptedDataKey: Buffer.from(generatorKeyId) + }) + + const material = await testKeyring.onDecrypt( + new NodeDecryptionMaterial(suite), + [edk], + context + ) + + expect(material.hasUnencryptedDataKey).to.equal(true) + + expect(material.keyringTrace).to.have.lengthOf(1) + const [traceDecrypt] = material.keyringTrace + expect(traceDecrypt.keyNamespace).to.equal('aws-kms') + expect(traceDecrypt.keyName).to.equal(generatorKeyId) + expect(traceDecrypt.flags & KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY).to.equal(KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY) + expect(traceDecrypt.flags & KeyringTraceFlag.WRAPPING_KEY_VERIFIED_ENC_CTX).to.equal(KeyringTraceFlag.WRAPPING_KEY_VERIFIED_ENC_CTX) + }) + + it('decrypt errors should not halt', async () => { + const generatorKeyId = 'arn:aws:kms:us-east-1:123456789012:alias/example-alias' + const context = { some: 'context' } + const grantTokens = 'grant' + const discovery = true + const suite = new NodeAlgorithmSuite(AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16) + + const clientProvider: any = () => { + return { decrypt } + function decrypt () { + throw new Error('failed to decrypt') + } + } + class TestKmsKeyring extends KmsKeyringClass(Keyring as KeyRingConstructible) {} + + const testKeyring = new TestKmsKeyring({ + clientProvider, + grantTokens, + discovery + }) + + const edk = new EncryptedDataKey({ + providerId: 'aws-kms', + providerInfo: generatorKeyId, + encryptedDataKey: Buffer.from(generatorKeyId) + }) + + const material = await testKeyring.onDecrypt( + new NodeDecryptionMaterial(suite), + [edk], + context + ) + + expect(material.hasUnencryptedDataKey).to.equal(false) + expect(material.keyringTrace).to.have.lengthOf(0) + }) + }) diff --git a/modules/kms-keyring/test/kms_keyring.onencrypt.test.ts b/modules/kms-keyring/test/kms_keyring.onencrypt.test.ts new file mode 100644 index 000000000..e294a1676 --- /dev/null +++ b/modules/kms-keyring/test/kms_keyring.onencrypt.test.ts @@ -0,0 +1,215 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use + * this file except in compliance with the License. A copy of the License is + * located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* eslint-env mocha */ + +import chai from 'chai' +import chaiAsPromised from 'chai-as-promised' +import 'mocha' +import { + KmsKeyringClass, + KeyRingConstructible // eslint-disable-line no-unused-vars +} from '../src/kms_keyring' +import { + NodeAlgorithmSuite, + AlgorithmSuiteIdentifier, + NodeEncryptionMaterial, + KeyringTraceFlag, + Keyring +} from '@aws-crypto/material-management' +import { GenerateDataKeyInput } from '../src/kms_types/GenerateDataKeyInput' // eslint-disable-line no-unused-vars +import { EncryptInput } from '../src/kms_types/EncryptInput' // eslint-disable-line no-unused-vars +chai.use(chaiAsPromised) +const { expect } = chai + +describe('KmsKeyring: _onEncrypt', () => { + it('returns material', async () => { + const generatorKeyId = 'arn:aws:kms:us-east-1:123456789012:alias/example-alias' + const encryptKmsKey = 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012' + const keyIds = [encryptKmsKey] + const context = { some: 'context' } + const grantTokens = 'grant' + const suite = new NodeAlgorithmSuite(AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16) + + const clientProvider: any = () => { + return { generateDataKey, encrypt } + function generateDataKey ({ KeyId, EncryptionContext, GrantTokens }: GenerateDataKeyInput) { + expect(EncryptionContext === context).to.equal(true) + expect(GrantTokens).to.equal(grantTokens) + return { + Plaintext: new Uint8Array(suite.keyLengthBytes), + KeyId, + CiphertextBlob: new Uint8Array(5) + } + } + function encrypt ({ KeyId, EncryptionContext, GrantTokens }: EncryptInput) { + expect(EncryptionContext === context).to.equal(true) + expect(GrantTokens).to.equal(grantTokens) + return { + KeyId, + CiphertextBlob: new Uint8Array(5) + } + } + } + class TestKmsKeyring extends KmsKeyringClass(Keyring as KeyRingConstructible) {} + + const testKeyring = new TestKmsKeyring({ + clientProvider, + generatorKeyId, + keyIds, + grantTokens + }) + + const material = await testKeyring.onEncrypt(new NodeEncryptionMaterial(suite), context) + + expect(material.hasUnencryptedDataKey).to.equal(true) + + expect(material.encryptedDataKeys).to.have.lengthOf(2) + const [edkGenerate, edkEncrypt] = material.encryptedDataKeys + expect(edkGenerate.providerId).to.equal('aws-kms') + expect(edkGenerate.providerInfo).to.equal(generatorKeyId) + expect(edkEncrypt.providerId).to.equal('aws-kms') + expect(edkEncrypt.providerInfo).to.equal(encryptKmsKey) + + expect(material.keyringTrace).to.have.lengthOf(2) + const [traceGenerate, traceEncrypt] = material.keyringTrace + expect(traceGenerate.keyNamespace).to.equal('aws-kms') + expect(traceGenerate.keyName).to.equal(generatorKeyId) + expect(traceGenerate.flags & KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY).to.equal(KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY) + expect(traceGenerate.flags & KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY).to.equal(KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY) + expect(traceGenerate.flags & KeyringTraceFlag.WRAPPING_KEY_SIGNED_ENC_CTX).to.equal(KeyringTraceFlag.WRAPPING_KEY_SIGNED_ENC_CTX) + expect(traceEncrypt.keyNamespace).to.equal('aws-kms') + expect(traceEncrypt.keyName).to.equal(encryptKmsKey) + expect(traceEncrypt.flags & KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY).to.equal(KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY) + expect(traceEncrypt.flags & KeyringTraceFlag.WRAPPING_KEY_SIGNED_ENC_CTX).to.equal(KeyringTraceFlag.WRAPPING_KEY_SIGNED_ENC_CTX) + }) + + it('Precondition: A generatorKeyId must generate if we do not have an unencrypted data key.', async () => { + const generatorKeyId = 'arn:aws:kms:us-east-1:123456789012:alias/example-alias' + const encryptKmsKey = 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012' + const keyIds = [encryptKmsKey] + const context = { some: 'context' } + const grantTokens = 'grant' + const suite = new NodeAlgorithmSuite(AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16) + + const clientProvider: any = () => { + return false + } + class TestKmsKeyring extends KmsKeyringClass(Keyring as KeyRingConstructible) {} + + const testKeyring = new TestKmsKeyring({ + clientProvider, + generatorKeyId, + keyIds, + grantTokens + }) + + await expect(testKeyring.onEncrypt(new NodeEncryptionMaterial(suite), context)) + .to.rejectedWith(Error) + }) + + it('Precondition: If a generator does not exist, an unencryptedDataKey *must* already exist.', async () => { + const encryptKmsKey = 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012' + const keyIds = [encryptKmsKey] + const context = { some: 'context' } + const grantTokens = 'grant' + const suite = new NodeAlgorithmSuite(AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16) + + const clientProvider: any = () => { + return false + } + class TestKmsKeyring extends KmsKeyringClass(Keyring as KeyRingConstructible) {} + + const testKeyring = new TestKmsKeyring({ + clientProvider, + keyIds, + grantTokens + }) + + await expect(testKeyring.onEncrypt(new NodeEncryptionMaterial(suite), context)) + .to.rejectedWith(Error) + }) + + it('generator should encrypt if material already generated', async () => { + const generatorKeyId = 'arn:aws:kms:us-east-1:123456789012:alias/example-alias' + const suite = new NodeAlgorithmSuite(AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16) + + const clientProvider: any = () => { + return { encrypt } + function encrypt ({ KeyId }: EncryptInput) { + return { + KeyId, + CiphertextBlob: new Uint8Array(5) + } + } + } + class TestKmsKeyring extends KmsKeyringClass(Keyring as KeyRingConstructible) {} + + const testKeyring = new TestKmsKeyring({ + clientProvider, + generatorKeyId + }) + + const seedMaterial = new NodeEncryptionMaterial(suite) + .setUnencryptedDataKey(new Uint8Array(suite.keyLengthBytes), { + keyName: 'keyName', + keyNamespace: 'keyNamespace', + flags: KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY + }) + + const material = await testKeyring.onEncrypt(seedMaterial) + + // only setUnencryptedDataKey on seedMaterial + expect(material.encryptedDataKeys).to.have.lengthOf(1) + const [kmsEDK] = material.encryptedDataKeys + expect(kmsEDK.providerId).to.equal('aws-kms') + expect(kmsEDK.providerInfo).to.equal(generatorKeyId) + + expect(material.keyringTrace).to.have.lengthOf(2) + const [, kmsTrace] = material.keyringTrace + expect(kmsTrace.keyNamespace).to.equal('aws-kms') + expect(kmsTrace.keyName).to.equal(generatorKeyId) + expect(kmsTrace.flags & KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY).to.equal(KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY) + expect(kmsTrace.flags & KeyringTraceFlag.WRAPPING_KEY_SIGNED_ENC_CTX).to.equal(KeyringTraceFlag.WRAPPING_KEY_SIGNED_ENC_CTX) + }) + + it('clientProvider may not return a client, in this case there is not an EDK to add', async () => { + const generatorKeyId = 'arn:aws:kms:us-east-1:123456789012:alias/example-alias' + const suite = new NodeAlgorithmSuite(AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16) + + const clientProvider: any = () => { + return false + } + class TestKmsKeyring extends KmsKeyringClass(Keyring as KeyRingConstructible) {} + + const testKeyring = new TestKmsKeyring({ + clientProvider, + generatorKeyId + }) + + const seedMaterial = new NodeEncryptionMaterial(suite) + .setUnencryptedDataKey(new Uint8Array(suite.keyLengthBytes), { + keyName: 'keyName', + keyNamespace: 'keyNamespace', + flags: KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY + }) + + const material = await testKeyring.onEncrypt(seedMaterial) + + // only setUnencryptedDataKey on seedMaterial + expect(material.encryptedDataKeys).to.have.lengthOf(0) + expect(material.keyringTrace).to.have.lengthOf(1) + }) +}) diff --git a/modules/kms-keyring/test/region_from_kms_key_arn.test.ts b/modules/kms-keyring/test/region_from_kms_key_arn.test.ts new file mode 100644 index 000000000..2f0b8ad37 --- /dev/null +++ b/modules/kms-keyring/test/region_from_kms_key_arn.test.ts @@ -0,0 +1,44 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use + * this file except in compliance with the License. A copy of the License is + * located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* eslint-env mocha */ + +import { expect } from 'chai' +import 'mocha' +import { regionFromKmsKeyArn } from '../src/region_from_kms_key_arn' + +describe('regionFromKmsKeyArn', () => { + it('return region', () => { + const test1 = regionFromKmsKeyArn('arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012') + expect(test1).to.equal('us-east-1') + const test2 = regionFromKmsKeyArn('arn:aws:kms:us-east-1:123456789012:alias/example-alias') + expect(test2).to.equal('us-east-1') + }) + + it('Precondition: A KMS key arn must be a string.', () => { + const bad = {} as any + expect(() => regionFromKmsKeyArn(bad)).to.throw() + }) + + it('Postcondition: The ARN must be well formed.', () => { + expect(() => regionFromKmsKeyArn('')).to.throw() + expect(() => regionFromKmsKeyArn('NOTarn:aws:kms:us-east-1:123456789012:alias/example-alias')).to.throw() + // empty partition + expect(() => regionFromKmsKeyArn('arn::kms:us-east-1:123456789012:alias/example-alias')).to.throw() + expect(() => regionFromKmsKeyArn('arn:aws:NOTkms:us-east-1:123456789012:alias/example-alias')).to.throw() + // empty region + expect(() => regionFromKmsKeyArn('arn:aws:kms::123456789012:alias/example-alias')).to.throw() + }) +}) diff --git a/modules/kms-keyring/tsconfig.json b/modules/kms-keyring/tsconfig.json new file mode 100644 index 000000000..a5946afbe --- /dev/null +++ b/modules/kms-keyring/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../tsconfig.settings.json", + "compilerOptions": { + "outDir": "build/main", + "rootDir": "./src" + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules/**"], + "references": [ + { "path": "../material-management" } + ] +} \ No newline at end of file diff --git a/modules/kms-keyring/tsconfig.module.json b/modules/kms-keyring/tsconfig.module.json new file mode 100644 index 000000000..50bf04db4 --- /dev/null +++ b/modules/kms-keyring/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