From 1cd2410ab2e73e501a26e65cd41138b9f90ef3c7 Mon Sep 17 00:00:00 2001 From: seebees Date: Wed, 10 Jul 2019 21:50:46 -0700 Subject: [PATCH] fix: aws sdk version dependencies pollution resolves #136 resolved #138 The AWS SDK for JS v2 and v3 have similar types, but they are not exactly the same. In a effort to have both versions work, I imported as dev dependencies all the relevant packages and tried to use conditional types. However, since the types are all exported by the functions this did not work as planed and caused _every_ client to want _every_ SDK. The interface the AWS Encryption SDK for JS needs from the AWS SDK is minimal. Hand rolling the appropriate types is the best solution at this time. --- .../kms-keyring-node/src/kms_keyring_node.ts | 2 +- modules/kms-keyring/package.json | 4 - modules/kms-keyring/src/helpers.ts | 90 +++++++-------- .../kms-keyring/src/kms_client_supplier.ts | 36 +++--- modules/kms-keyring/src/kms_keyring.ts | 14 +-- modules/kms-keyring/src/kms_types.ts | 108 ++++++++---------- 6 files changed, 113 insertions(+), 141 deletions(-) diff --git a/modules/kms-keyring-node/src/kms_keyring_node.ts b/modules/kms-keyring-node/src/kms_keyring_node.ts index 085caf910..8db5e097c 100644 --- a/modules/kms-keyring-node/src/kms_keyring_node.ts +++ b/modules/kms-keyring-node/src/kms_keyring_node.ts @@ -49,4 +49,4 @@ export class KmsKeyringNode extends KmsKeyringClass(KeyringNode as KeyRingConstr } immutableClass(KmsKeyringNode) -export { getKmsClient, cacheKmsClients, limitRegions, excludeRegions, cacheClients } +export { getKmsClient, cacheKmsClients, getClient, limitRegions, excludeRegions, cacheClients, KMS } diff --git a/modules/kms-keyring/package.json b/modules/kms-keyring/package.json index 8564f82c6..85248c221 100644 --- a/modules/kms-keyring/package.json +++ b/modules/kms-keyring/package.json @@ -17,19 +17,15 @@ "license": "Apache-2.0", "dependencies": { "@aws-crypto/material-management": "^0.2.0-preview.1", - "@aws-sdk/types": "0.1.0-preview.1", "tslib": "^1.9.3" }, "devDependencies": { - "@aws-sdk/client-kms-browser": "^0.1.0-preview.2", - "@aws-sdk/client-kms-node": "^0.1.0-preview.2", "@types/chai": "^4.1.4", "@types/chai-as-promised": "^7.1.0", "@types/mocha": "^5.2.5", "@types/node": "^11.11.4", "@typescript-eslint/eslint-plugin": "^1.9.0", "@typescript-eslint/parser": "^1.9.0", - "aws-sdk": "^2.443.0", "chai": "^4.1.2", "chai-as-promised": "^7.1.1", "mocha": "^5.2.0", diff --git a/modules/kms-keyring/src/helpers.ts b/modules/kms-keyring/src/helpers.ts index 69c48e384..0336ec3c5 100644 --- a/modules/kms-keyring/src/helpers.ts +++ b/modules/kms-keyring/src/helpers.ts @@ -15,16 +15,13 @@ import { KmsClientSupplier } from './kms_client_supplier' // eslint-disable-line no-unused-vars import { - KMS, // eslint-disable-line no-unused-vars - GenerateDataKeyInput, // eslint-disable-line no-unused-vars - EncryptInput, // eslint-disable-line no-unused-vars - DecryptInput, // eslint-disable-line no-unused-vars - GenerateDataKeyOutput, // eslint-disable-line no-unused-vars - RequiredGenerateDataKeyOutput, // eslint-disable-line no-unused-vars - EncryptOutput, // eslint-disable-line no-unused-vars - RequiredEncryptOutput, // eslint-disable-line no-unused-vars - DecryptOutput, // eslint-disable-line no-unused-vars - RequiredDecryptOutput // eslint-disable-line no-unused-vars + AwsEsdkKMSInterface, // eslint-disable-line no-unused-vars + GenerateDataKeyResponse, // eslint-disable-line no-unused-vars + RequiredGenerateDataKeyResponse, // eslint-disable-line no-unused-vars + EncryptResponse, // eslint-disable-line no-unused-vars + RequiredEncryptResponse, // eslint-disable-line no-unused-vars + DecryptResponse, // eslint-disable-line no-unused-vars + RequiredDecryptResponse // eslint-disable-line no-unused-vars } from './kms_types' import { regionFromKmsKeyArn } from './region_from_kms_key_arn' import { @@ -35,55 +32,55 @@ import { export const KMS_PROVIDER_ID = 'aws-kms' -export async function generateDataKey ( +export async function generateDataKey ( clientProvider: KmsClientSupplier, NumberOfBytes: number, KeyId: string, EncryptionContext?: EncryptionContext, GrantTokens?: string[] -): Promise { +): Promise { const region = regionFromKmsKeyArn(KeyId) const client = clientProvider(region) /* Check for early return (Postcondition): Client region was not provided. */ if (!client) return false - const request: GenerateDataKeyInput = { KeyId, GrantTokens, NumberOfBytes, EncryptionContext } as GenerateDataKeyInput - // @ts-ignore - const v2vsV3Response = client.generateDataKey(request) - const v2vsV3Promise = (v2vsV3Response.promise ? v2vsV3Response.promise() : v2vsV3Response) - const dataKey: GenerateDataKeyOutput = await v2vsV3Promise + const v2vsV3Response = client.generateDataKey({ KeyId, GrantTokens, NumberOfBytes, EncryptionContext }) + const v2vsV3Promise = 'promise' in v2vsV3Response + ? v2vsV3Response.promise() + : v2vsV3Response + const dataKey = await v2vsV3Promise return safeGenerateDataKey(dataKey) } -export async function encrypt ( +export async function encrypt ( clientProvider: KmsClientSupplier, Plaintext: Uint8Array, KeyId: string, EncryptionContext?: EncryptionContext, GrantTokens?: string[] -): Promise { +): Promise { const region = regionFromKmsKeyArn(KeyId) const client = clientProvider(region) /* Check for early return (Postcondition): Client region was not provided. */ if (!client) return false - const request: EncryptInput = { KeyId, Plaintext, EncryptionContext, GrantTokens } as EncryptInput - // @ts-ignore - const v2vsV3Response = client.encrypt(request) - const v2vsV3Promise = (v2vsV3Response.promise ? v2vsV3Response.promise() : v2vsV3Response) - const kmsEDK: EncryptOutput = await v2vsV3Promise + const v2vsV3Response = client.encrypt({ KeyId, Plaintext, EncryptionContext, GrantTokens }) + const v2vsV3Promise = 'promise' in v2vsV3Response + ? v2vsV3Response.promise() + : v2vsV3Response + const kmsEDK = await v2vsV3Promise return safeEncryptOutput(kmsEDK) } -export async function decrypt ( +export async function decrypt ( clientProvider: KmsClientSupplier, { providerId, providerInfo, encryptedDataKey }: EncryptedDataKey, EncryptionContext?: EncryptionContext, GrantTokens?: string[] -): Promise { +): Promise { /* Precondition: The EDK must be a KMS edk. */ needs(providerId === KMS_PROVIDER_ID, 'Unsupported providerId') const region = regionFromKmsKeyArn(providerInfo) @@ -91,14 +88,11 @@ export async function decrypt ( /* Check for early return (Postcondition): Client region was not provided. */ if (!client) return false - const request: DecryptInput = { - CiphertextBlob: encryptedDataKey, - EncryptionContext, - GrantTokens } as DecryptInput - // @ts-ignore - const v2vsV3Response = client.decrypt(request) - const v2vsV3Promise = (v2vsV3Response.promise ? v2vsV3Response.promise() : v2vsV3Response) - const dataKey: DecryptOutput = await v2vsV3Promise + const v2vsV3Response = client.decrypt({ CiphertextBlob: encryptedDataKey, EncryptionContext, GrantTokens }) + const v2vsV3Promise = 'promise' in v2vsV3Response + ? v2vsV3Response.promise() + : v2vsV3Response + const dataKey = await v2vsV3Promise return safeDecryptOutput(dataKey) } @@ -106,42 +100,42 @@ export async function decrypt ( export function kmsResponseToEncryptedDataKey ({ KeyId: providerInfo, CiphertextBlob: encryptedDataKey -}: RequiredEncryptOutput) { +}: RequiredEncryptResponse) { return new EncryptedDataKey({ providerId: KMS_PROVIDER_ID, providerInfo, encryptedDataKey }) } -function safeGenerateDataKey ( - dataKey: GenerateDataKeyOutput -): RequiredGenerateDataKeyOutput { +function safeGenerateDataKey ( + dataKey: GenerateDataKeyResponse +): RequiredGenerateDataKeyResponse { /* Postcondition: KMS must return serializable generate data key. */ needs(typeof dataKey.KeyId === 'string' && dataKey.Plaintext instanceof Uint8Array && dataKey.CiphertextBlob instanceof Uint8Array, 'Malformed KMS response.') - return safePlaintext(dataKey) + return safePlaintext(dataKey) } -function safeEncryptOutput ( - dataKey: EncryptOutput -): RequiredEncryptOutput { +function safeEncryptOutput ( + dataKey: EncryptResponse +): RequiredEncryptResponse { /* Postcondition: KMS must return serializable encrypted data key. */ needs(typeof dataKey.KeyId === 'string' && dataKey.CiphertextBlob instanceof Uint8Array, 'Malformed KMS response.') - return dataKey + return dataKey } -function safeDecryptOutput ( - dataKey: DecryptOutput -): RequiredDecryptOutput { +function safeDecryptOutput ( + dataKey: DecryptResponse +): RequiredDecryptResponse { /* Postcondition: KMS must return usable decrypted key. */ needs(typeof dataKey.KeyId === 'string' && dataKey.Plaintext instanceof Uint8Array, 'Malformed KMS response.') - return safePlaintext(dataKey) + return safePlaintext(dataKey) } -function safePlaintext (dataKey: RequiredDecryptOutput | RequiredGenerateDataKeyOutput): RequiredDecryptOutput | RequiredGenerateDataKeyOutput { +function safePlaintext (dataKey: RequiredDecryptResponse | RequiredGenerateDataKeyResponse): RequiredDecryptResponse | RequiredGenerateDataKeyResponse { /* The KMS Client *may* return a Buffer that is not isolated. * i.e. the byteOffset !== 0. * This means that the unencrypted data key is possibly accessible to someone else. diff --git a/modules/kms-keyring/src/kms_client_supplier.ts b/modules/kms-keyring/src/kms_client_supplier.ts index 9f7a8cdc9..5f2370773 100644 --- a/modules/kms-keyring/src/kms_client_supplier.ts +++ b/modules/kms-keyring/src/kms_client_supplier.ts @@ -15,27 +15,25 @@ import { needs } from '@aws-crypto/material-management' import { - KMS, // eslint-disable-line no-unused-vars - KMSConfiguration, // eslint-disable-line no-unused-vars - KMSOperations // eslint-disable-line no-unused-vars + AwsEsdkKMSInterface // eslint-disable-line no-unused-vars } from './kms_types' -interface KMSConstructibleNonOption { +interface KMSConstructibleNonOption { new(config: Config) : Client } -interface KMSConstructibleOption { +interface KMSConstructibleOption { new(config?: Config) : Client } -export type KMSConstructible = KMSConstructibleNonOption | KMSConstructibleOption +export type KMSConstructible = KMSConstructibleNonOption | KMSConstructibleOption -export interface KmsClientSupplier { +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 ( +export function getClient ( KMSClient: KMSConstructible, defaultConfig?: Config ): KmsClientSupplier { @@ -58,7 +56,7 @@ export function getClient ( } } -export function limitRegions ( +export function limitRegions ( regions: string[], getClient: KmsClientSupplier ): KmsClientSupplier { @@ -71,7 +69,7 @@ export function limitRegions ( } } -export function excludeRegions ( +export function excludeRegions ( regions: string[], getClient: KmsClientSupplier ): KmsClientSupplier { @@ -84,7 +82,7 @@ export function excludeRegions ( } } -export function cacheClients ( +export function cacheClients ( getClient: KmsClientSupplier ): KmsClientSupplier { const clientsCache: {[key: string]: Client|false} = {} @@ -103,7 +101,7 @@ export function cacheClients ( * This does *not* mean that this call is successful, * only that the region is backed by a functional KMS service. */ -function deferCache ( +function deferCache ( clientsCache: {[key: string]: Client|false}, region: string, client: Client|false @@ -115,17 +113,17 @@ function deferCache ( } const { encrypt, decrypt, generateDataKey } = client - return (['encrypt', 'decrypt', 'generateDataKey']).reduce(wrapOperation, client) + return (<(keyof AwsEsdkKMSInterface)[]>['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 + function wrapOperation (client: Client, name: keyof AwsEsdkKMSInterface): Client { const original = client[name] - client[name] = function (...args: any): Promise { + client[name] = function wrappedOperation (this: Client, args: any): Promise { // @ts-ignore (there should be a TypeScript solution for this) - const v2vsV3Response = original.apply(client, args) - const v2vsV3Promise = (v2vsV3Response.promise ? v2vsV3Response.promise() : v2vsV3Response) + const v2vsV3Response = original.call(client, args) + const v2vsV3Promise = 'promise' in v2vsV3Response + ? v2vsV3Response.promise() + : v2vsV3Response return v2vsV3Promise .then((response: any) => { clientsCache[region] = Object.assign(client, { encrypt, decrypt, generateDataKey }) diff --git a/modules/kms-keyring/src/kms_keyring.ts b/modules/kms-keyring/src/kms_keyring.ts index 4795805ae..8b07edc2d 100644 --- a/modules/kms-keyring/src/kms_keyring.ts +++ b/modules/kms-keyring/src/kms_keyring.ts @@ -15,8 +15,8 @@ import { KmsClientSupplier } from './kms_client_supplier' // eslint-disable-line no-unused-vars import { - KMS, // eslint-disable-line no-unused-vars - RequiredDecryptOutput // eslint-disable-line no-unused-vars + AwsEsdkKMSInterface, // eslint-disable-line no-unused-vars + RequiredDecryptResponse // eslint-disable-line no-unused-vars } from './kms_types' import { needs, @@ -34,7 +34,7 @@ import { import { KMS_PROVIDER_ID, generateDataKey, encrypt, decrypt, kmsResponseToEncryptedDataKey } from './helpers' import { regionFromKmsKeyArn } from './region_from_kms_key_arn' -export interface KmsKeyringInput { +export interface KmsKeyringInput { clientProvider: KmsClientSupplier keyIds?: string[] generatorKeyId?: string @@ -42,7 +42,7 @@ export interface KmsKeyringInput { discovery?: boolean } -export interface KeyRing extends Keyring { +export interface KeyRing extends Keyring { keyIds: ReadonlyArray generatorKeyId?: string clientProvider: KmsClientSupplier @@ -52,7 +52,7 @@ export interface KeyRing _onDecrypt(material: DecryptionMaterial, encryptedDataKeys: EncryptedDataKey[], context?: EncryptionContext): Promise> } -export interface KmsKeyRingConstructible { +export interface KmsKeyRingConstructible { new(input: KmsKeyringInput): KeyRing } @@ -60,7 +60,7 @@ export interface KeyRingConstructible { new(): Keyring } -export function KmsKeyringClass ( +export function KmsKeyringClass ( BaseKeyring: KeyRingConstructible ): KmsKeyRingConstructible { class KmsKeyring extends BaseKeyring implements KeyRing { @@ -160,7 +160,7 @@ export function KmsKeyringClass = Client extends KMSv3Node - ? import('@aws-sdk/client-kms-node').GenerateDataKeyInput - : Client extends KMSv3Browser - ? import('@aws-sdk/client-kms-browser').GenerateDataKeyInput - : Client extends KMSv2 - ? import('aws-sdk').KMS.GenerateDataKeyRequest - : never - -export type EncryptInput = Client extends KMSv3Node - ? import('@aws-sdk/client-kms-node').EncryptInput - : Client extends KMSv3Browser - ? import('@aws-sdk/client-kms-browser').EncryptInput - : Client extends KMSv2 - ? import('aws-sdk').KMS.EncryptRequest - : never - -export type DecryptInput = Client extends KMSv3Node - ? import('@aws-sdk/client-kms-node').DecryptInput - : Client extends KMSv3Browser - ? import('@aws-sdk/client-kms-browser').DecryptInput - : Client extends KMSv2 - ? import('aws-sdk').KMS.DecryptRequest - : never +export interface DecryptRequest { + CiphertextBlob: Uint8Array + EncryptionContext?: EncryptionContext + GrantTokens?: string[] +} -export type GenerateDataKeyOutput = Client extends KMSv3Node - ? import('@aws-sdk/client-kms-node').GenerateDataKeyOutput - : Client extends KMSv3Browser - ? import('@aws-sdk/client-kms-browser').GenerateDataKeyOutput - : Client extends KMSv2 - ? import('aws-sdk').KMS.GenerateDataKeyResponse - : never +interface Blob {} +type Data = string | Buffer | Uint8Array | Blob +export interface DecryptResponse { + KeyId?: string + Plaintext?: Data +} -export interface RequiredGenerateDataKeyOutput { - CiphertextBlob: Uint8Array +export interface RequiredDecryptResponse extends Required{ Plaintext: Uint8Array - KeyId: string } -export type EncryptOutput = Client extends KMSv3Node - ? import('@aws-sdk/client-kms-node').EncryptOutput - : Client extends KMSv3Browser - ? import('@aws-sdk/client-kms-browser').EncryptOutput - : Client extends KMSv2 - ? import('aws-sdk').KMS.EncryptResponse - : never +export interface EncryptRequest { + KeyId: string + Plaintext: Uint8Array + EncryptionContext?: EncryptionContext + GrantTokens?: string[] +} +export interface EncryptResponse { + CiphertextBlob?: Data + KeyId?: string +} -export interface RequiredEncryptOutput { +export interface RequiredEncryptResponse extends Required{ CiphertextBlob: Uint8Array +} + +export interface GenerateDataKeyRequest { KeyId: string + EncryptionContext?: EncryptionContext + NumberOfBytes?: number + GrantTokens?: string[] } -export type DecryptOutput = Client extends KMSv3Node - ? import('@aws-sdk/client-kms-node').DecryptOutput - : Client extends KMSv3Browser - ? import('@aws-sdk/client-kms-browser').DecryptOutput - : Client extends KMSv2 - ? import('aws-sdk').KMS.DecryptResponse - : never +export interface GenerateDataKeyResponse { + CiphertextBlob?: Data + Plaintext?: Data + KeyId?: string +} -export interface RequiredDecryptOutput { - KeyId: string +export interface RequiredGenerateDataKeyResponse extends Required{ + CiphertextBlob: Uint8Array Plaintext: Uint8Array } -export type KMSOperations = keyof Pick +export interface AwsSdkV2Response { + promise(): Promise +} + +export interface AwsEsdkKMSInterface { + decrypt(args: DecryptRequest): Promise|AwsSdkV2Response + encrypt(args: EncryptRequest): Promise|AwsSdkV2Response + generateDataKey(args: GenerateDataKeyRequest): Promise|AwsSdkV2Response +}