modules/kms-keyring/src/kms_mrk_discovery_keyring.ts (212 lines of code) (raw):
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import {
DecryptionMaterial,
EncryptedDataKey,
EncryptionMaterial,
immutableClass,
Keyring,
KeyringTrace,
KeyringTraceFlag,
needs,
readOnlyProperty,
SupportedAlgorithmSuites,
Newable,
Catchable,
} from '@aws-crypto/material-management'
import {
constructArnInOtherRegion,
isMultiRegionAwsKmsArn,
parseAwsKmsKeyArn,
} from './arn_parsing'
import { decrypt, KMS_PROVIDER_ID } from './helpers'
import { AwsEsdkKMSInterface, RequiredDecryptResponse } from './kms_types'
export interface AwsKmsMrkAwareSymmetricDiscoveryKeyringInput<
Client extends AwsEsdkKMSInterface
> {
client: Client
discoveryFilter?: Readonly<{
accountIDs: readonly string[]
partition: string
}>
grantTokens?: string[]
}
export interface IAwsKmsMrkAwareSymmetricDiscoveryKeyring<
S extends SupportedAlgorithmSuites,
Client extends AwsEsdkKMSInterface
> extends Keyring<S> {
client: Client
clientRegion: string
discoveryFilter?: Readonly<{
accountIDs: readonly string[]
partition: string
}>
grantTokens?: string[]
_onEncrypt(material: EncryptionMaterial<S>): Promise<EncryptionMaterial<S>>
_onDecrypt(
material: DecryptionMaterial<S>,
encryptedDataKeys: EncryptedDataKey[]
): Promise<DecryptionMaterial<S>>
}
export interface AwsKmsMrkAwareSymmetricDiscoveryKeyringConstructible<
S extends SupportedAlgorithmSuites,
Client extends AwsEsdkKMSInterface
> {
new (
input: AwsKmsMrkAwareSymmetricDiscoveryKeyringInput<Client>
): IAwsKmsMrkAwareSymmetricDiscoveryKeyring<S, Client>
}
export function AwsKmsMrkAwareSymmetricDiscoveryKeyringClass<
S extends SupportedAlgorithmSuites,
Client extends AwsEsdkKMSInterface
>(
BaseKeyring: Newable<Keyring<S>>
): AwsKmsMrkAwareSymmetricDiscoveryKeyringConstructible<S, Client> {
class AwsKmsMrkAwareSymmetricDiscoveryKeyring
//= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.5
//# MUST implement that AWS Encryption SDK Keyring interface (../keyring-
//# interface.md#interface)
extends BaseKeyring
implements IAwsKmsMrkAwareSymmetricDiscoveryKeyring<S, Client>
{
public declare client: Client
public declare clientRegion: string
public declare grantTokens?: string[]
public declare discoveryFilter?: Readonly<{
accountIDs: readonly string[]
partition: string
}>
//= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.6
//# On initialization the caller MUST provide:
constructor({
client,
grantTokens,
discoveryFilter,
}: AwsKmsMrkAwareSymmetricDiscoveryKeyringInput<Client>) {
super()
//= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.6
//# The keyring MUST know what Region the AWS KMS client is in.
//
//= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.6
//# It
//# SHOULD obtain this information directly from the client as opposed to
//# having an additional parameter.
//
//= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.6
//# However if it can not, then it MUST
//# NOT create the client itself.
//
//= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.6
//# It SHOULD have a Region parameter and
//# SHOULD try to identify mismatched configurations.
//
// @ts-ignore the V3 client has set the config to protected
const clientRegion = client.config.region
needs(clientRegion, 'Client must be configured to a region.')
/* Precondition: The AwsKmsMrkAwareSymmetricDiscoveryKeyring Discovery filter *must* be able to match something.
* I am not going to wait to tell you
* that no CMK can match an empty account list.
* e.g. [], [''], '' are not valid.
*/
needs(
!discoveryFilter ||
(discoveryFilter.accountIDs &&
discoveryFilter.accountIDs.length &&
!!discoveryFilter.partition &&
discoveryFilter.accountIDs.every(
(a) => typeof a === 'string' && !!a
)),
'A discovery filter must be able to match something.'
)
readOnlyProperty(this, 'client', client)
// AWS SDK v3 stores the clientRegion behind an async function
if (typeof clientRegion == 'function') {
/* Postcondition: Store the AWS SDK V3 region promise as the clientRegion.
* This information MUST be communicated to OnDecrypt.
* If a caller creates a keyring,
* and then calls OnDecrypt all in synchronous code
* then the v3 region will not have been able to resolve.
* If clientRegion was null,
* then the keyring would filter out all EDKs
* because the region does not match.
*/
this.clientRegion = clientRegion().then((region: string) => {
/* Postcondition: Resolve the AWS SDK V3 region promise and update clientRegion. */
readOnlyProperty(this, 'clientRegion', region)
/* Postcondition: Resolve the promise with the value set. */
return region
})
} else {
readOnlyProperty(this, 'clientRegion', clientRegion)
}
readOnlyProperty(this, 'grantTokens', grantTokens)
readOnlyProperty(
this,
'discoveryFilter',
discoveryFilter
? Object.freeze({
...discoveryFilter,
accountIDs: Object.freeze(discoveryFilter.accountIDs),
})
: discoveryFilter
)
}
async _onEncrypt(): Promise<EncryptionMaterial<S>> {
//= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.7
//# This function MUST fail.
throw new Error(
'AwsKmsMrkAwareSymmetricDiscoveryKeyring cannot be used to encrypt'
)
}
//= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8
//# OnDecrypt MUST take decryption materials (structures.md#decryption-
//# materials) and a list of encrypted data keys
//# (structures.md#encrypted-data-key) as input.
async _onDecrypt(
material: DecryptionMaterial<S>,
encryptedDataKeys: EncryptedDataKey[]
): Promise<DecryptionMaterial<S>> {
//= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8
//# If the decryption materials (structures.md#decryption-materials)
//# already contained a valid plaintext data key OnDecrypt MUST
//# immediately return the unmodified decryption materials
//# (structures.md#decryption-materials).
if (material.hasValidKey()) return material
// See the constructor, this is to support both AWS SDK v2 and v3.
needs(
typeof this.clientRegion === 'string' ||
/* Precondition: AWS SDK V3 region promise MUST have resolved to a string.
* In the constructor the region promise resolves
* to the same value that is then set.
*/
// @ts-ignore
typeof (await this.clientRegion) == 'string',
'clientRegion MUST be a string.'
)
const { client, grantTokens, clientRegion } = this
//= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8
//# The set of encrypted data keys MUST first be filtered to match this
//# keyring's configuration.
const decryptableEDKs = encryptedDataKeys.filter(filterEDKs(this))
const cmkErrors: Catchable[] = []
//= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8
//# For each encrypted data key in the filtered set, one at a time, the
//# OnDecrypt MUST attempt to decrypt the data key.
for (const edk of decryptableEDKs) {
//= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8
//# Otherwise it MUST
//# be the provider info.
let keyId = edk.providerInfo
//= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8
//# * "KeyId": If the provider info's resource type is "key" and its
//# resource is a multi-Region key then a new ARN MUST be created
//# where the region part MUST equal the AWS KMS client region and
//# every other part MUST equal the provider info.
const keyArn = parseAwsKmsKeyArn(edk.providerInfo)
needs(keyArn, 'Unexpected EDK ProviderInfo for AWS KMS EDK')
if (isMultiRegionAwsKmsArn(keyArn)) {
keyId = constructArnInOtherRegion(keyArn, clientRegion)
}
let dataKey: RequiredDecryptResponse | false = false
try {
//= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8
//# When calling AWS KMS Decrypt
//# (https://docs.aws.amazon.com/kms/latest/APIReference/
//# API_Decrypt.html), the keyring MUST call with a request constructed
//# as follows:
dataKey = await decrypt(
//= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8
//# To attempt to decrypt a particular encrypted data key
//# (structures.md#encrypted-data-key), OnDecrypt MUST call AWS KMS
//# Decrypt (https://docs.aws.amazon.com/kms/latest/APIReference/
//# API_Decrypt.html) with the configured AWS KMS client.
client,
{
providerId: edk.providerId,
providerInfo: keyId,
encryptedDataKey: edk.encryptedDataKey,
},
material.encryptionContext,
grantTokens
)
/* This should be impossible given that decrypt only returns false if the client supplier does
* or if the providerId is not "aws-kms", which we have already filtered out
*/
if (!dataKey) continue
//= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8
//# * The "KeyId" field in the response MUST equal the requested "KeyId"
needs(
dataKey.KeyId === keyId,
'KMS Decryption key does not match the requested key id.'
)
const flags =
KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY |
KeyringTraceFlag.WRAPPING_KEY_VERIFIED_ENC_CTX
const trace: KeyringTrace = {
keyNamespace: KMS_PROVIDER_ID,
keyName: dataKey.KeyId,
flags,
}
//= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8
//# * The length of the response's "Plaintext" MUST equal the key
//# derivation input length (algorithm-suites.md#key-derivation-input-
//# length) specified by the algorithm suite (algorithm-suites.md)
//# included in the input decryption materials
//# (structures.md#decryption-materials).
//
//= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8
//# Since the response does satisfies these requirements then OnDecrypt
//# MUST do the following with the response:
//
// setUnencryptedDataKey will throw if the plaintext does not match the algorithm suite requirements.
material.setUnencryptedDataKey(dataKey.Plaintext, trace)
return material
} catch (e) {
//= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8
//# If the response does not satisfies these requirements then an error
//# is collected and the next encrypted data key in the filtered set MUST
//# be attempted.
cmkErrors.push({ errPlus: e })
}
}
//= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8
//# If OnDecrypt fails to successfully decrypt any encrypted data key
//# (structures.md#encrypted-data-key), then it MUST yield an error that
//# includes all collected errors.
needs(
material.hasValidKey(),
[
`Unable to decrypt data key${
!decryptableEDKs.length ? ': No EDKs supplied' : ''
}.`,
...cmkErrors.map((e, i) => `Error #${i + 1} \n${e.errPlus.stack}`),
].join('\n')
)
return material
}
}
immutableClass(AwsKmsMrkAwareSymmetricDiscoveryKeyring)
return AwsKmsMrkAwareSymmetricDiscoveryKeyring
}
function filterEDKs<
S extends SupportedAlgorithmSuites,
Client extends AwsEsdkKMSInterface
>({
discoveryFilter,
clientRegion,
}: IAwsKmsMrkAwareSymmetricDiscoveryKeyring<S, Client>) {
return function filter({ providerId, providerInfo }: EncryptedDataKey) {
//= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8
//# * Its provider ID MUST exactly match the value "aws-kms".
if (providerId !== KMS_PROVIDER_ID) return false
//= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8
//# * The provider info MUST be a valid AWS KMS ARN (aws-kms-key-
//# arn.md#a-valid-aws-kms-arn) with a resource type of "key" or
//# OnDecrypt MUST fail.
const edkArn = parseAwsKmsKeyArn(providerInfo)
needs(
edkArn && edkArn.ResourceType === 'key',
'Unexpected EDK ProviderInfo for AWS KMS EDK'
)
const {
AccountId: account,
Partition: partition,
Region: edkRegion,
} = edkArn
//= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8
//# * If the provider info is not identified as a multi-Region key (aws-
//# kms-key-arn.md#identifying-an-aws-kms-multi-region-key), then the
//# provider info's Region MUST match the AWS KMS client region.
if (!isMultiRegionAwsKmsArn(edkArn) && clientRegion !== edkRegion) {
return false
}
//= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8
//# * If a discovery filter is configured, its partition and the
//# provider info partition MUST match.
//
//= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8
//# * If a discovery filter is configured, its set of accounts MUST
//# contain the provider info account.
return (
!discoveryFilter ||
(discoveryFilter.partition === partition &&
discoveryFilter.accountIDs.includes(account))
)
}
}