All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.amazonaws.encryptionsdk.kms.AwsKmsMrkAwareMasterKey Maven / Gradle / Ivy

// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package com.amazonaws.encryptionsdk.kms;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.function.Supplier;

import com.amazonaws.AmazonServiceException;
import com.amazonaws.AmazonWebServiceRequest;
import com.amazonaws.encryptionsdk.*;
import com.amazonaws.encryptionsdk.exception.AwsCryptoException;
import com.amazonaws.encryptionsdk.internal.AwsKmsCmkArnInfo;
import com.amazonaws.encryptionsdk.internal.VersionInfo;
import com.amazonaws.services.kms.AWSKMS;
import com.amazonaws.services.kms.model.DecryptRequest;
import com.amazonaws.services.kms.model.DecryptResult;
import com.amazonaws.services.kms.model.EncryptRequest;
import com.amazonaws.services.kms.model.EncryptResult;
import com.amazonaws.services.kms.model.GenerateDataKeyRequest;
import com.amazonaws.services.kms.model.GenerateDataKeyResult;

import static com.amazonaws.encryptionsdk.internal.AwsKmsCmkArnInfo.*;


//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.5
//# MUST implement the Master Key Interface (../master-key-
//# interface.md#interface)
//
//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.7
//# MUST be unchanged from the Master Key interface.
//
//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.8
//# MUST be unchanged from the Master Key interface.
/**
 * Represents a single Aws KMS key
 * and is used to encrypt/decrypt data with
 * {@link AwsCrypto}.
 * This key may be a multi region key,
 * in which case this component
 * is able to recognize
 * different regional replicas
 * of this multi region key as the same.
 */
public final class AwsKmsMrkAwareMasterKey extends MasterKey implements KmsMethods {
    private static final String USER_AGENT = VersionInfo.loadUserAgent();
    private final AWSKMS kmsClient_;
    private final List grantTokens_ = new ArrayList<>();
    private final String awsKmsIdentifier_;
    private final MasterKeyProvider sourceProvider_;

    private static  T updateUserAgent(T request) {
        request.getRequestClientOptions().appendUserAgent(USER_AGENT);

        return request;
    }

    /**
     * A light builder method.
     * 
     * @see KmsMasterKey#getInstance(Supplier, String, MasterKeyProvider)
     * @param kms An AWS KMS Client
     * @param awsKmsIdentifier An identifier for an AWS KMS key. May be a raw resource.
     */
    static AwsKmsMrkAwareMasterKey getInstance(
            final AWSKMS kms,
            final String awsKmsIdentifier,
            final MasterKeyProvider provider
    ) {
        return new AwsKmsMrkAwareMasterKey(awsKmsIdentifier, kms, provider);
    }

    //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.6
    //# On initialization, the caller MUST provide:
    private AwsKmsMrkAwareMasterKey(
            final String awsKmsIdentifier,
            final AWSKMS kmsClient,
            final MasterKeyProvider provider
    ) {

        //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.6
        //# The AWS KMS key identifier MUST NOT be null or empty.
        //
        //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.6
        //# The AWS KMS
        //# key identifier MUST be a valid identifier (aws-kms-key-arn.md#a-
        //# valid-aws-kms-identifier).
        validAwsKmsIdentifier(awsKmsIdentifier);

        //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.6
        //# The AWS KMS SDK client MUST not be null.
        if (kmsClient == null) {
            throw new IllegalArgumentException("AwsKmsMrkAwareMasterKey must be configured with an AWS KMS client.");
        }

        /* Precondition: A provider is required. */
        if (provider == null) {
            throw new IllegalArgumentException("AwsKmsMrkAwareMasterKey must be configured with a source provider.");
        }

        kmsClient_ = kmsClient;
        awsKmsIdentifier_ = awsKmsIdentifier;
        sourceProvider_ = provider;
    }

    @Override
    public String getProviderId() {
        return sourceProvider_.getDefaultProviderId();
    }

    @Override
    public String getKeyId() {
        return awsKmsIdentifier_;
    }

    //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.6
    //# The master key MUST be able to be configured with an optional list of
    //# Grant Tokens.
    //
    //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.6
    //= type=exception
    //# This configuration SHOULD be on initialization and
    //# SHOULD be immutable.
    // The existing KMS Master Key
    // sets grants in this way, so we continue this interface.
    /**
     * Clears and sets all grant tokens on this instance.
     * This is not thread safe.
     */
    @Override
    public void setGrantTokens(final List grantTokens) {
        grantTokens_.clear();
        grantTokens_.addAll(grantTokens);
    }

    @Override
    public List getGrantTokens() {
        return grantTokens_;
    }

    @Override
    public void addGrantToken(final String grantToken) {
        grantTokens_.add(grantToken);
    }

    //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.10
    //# The inputs MUST be the same as the Master Key Generate Data Key
    //# (../master-key-interface.md#generate-data-key) interface.
    /**
     * This is identical behavior to
     * @see KmsMasterKey#generateDataKey(CryptoAlgorithm, Map)
     */
    @Override
    public DataKey generateDataKey(final CryptoAlgorithm algorithm,
                                                            final Map encryptionContext) {
        //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.10
        //# This
        //# master key MUST use the configured AWS KMS client to make an AWS KMS
        //# GenerateDatakey (https://docs.aws.amazon.com/kms/latest/APIReference/
        //# API_GenerateDataKey.html) request constructed as follows:
        final GenerateDataKeyResult gdkResult = kmsClient_.generateDataKey(updateUserAgent(
                new GenerateDataKeyRequest()
                        .withKeyId(awsKmsIdentifier_)
                        .withNumberOfBytes(algorithm.getDataKeyLength())
                        .withEncryptionContext(encryptionContext)
                        .withGrantTokens(grantTokens_)
        ));
        //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.10
        //# If the call succeeds the AWS KMS Generate Data Key response's
        //# "Plaintext" MUST match the key derivation input length specified by
        //# the algorithm suite included in the input.
        if (gdkResult.getPlaintext().limit() != algorithm.getDataKeyLength()) {
            throw new IllegalStateException("Received an unexpected number of bytes from KMS");
        }

        final byte[] rawKey = new byte[algorithm.getDataKeyLength()];
        gdkResult.getPlaintext().get(rawKey);

        //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.10
        //# The response's "KeyId"
        //# MUST be valid.
        final String gdkResultKeyId = gdkResult.getKeyId();
        /* Exceptional Postcondition: Must have an AWS KMS ARN from AWS KMS generateDataKey. */
        if (parseInfoFromKeyArn(gdkResultKeyId) == null) {
            throw new IllegalStateException("Received an empty or invalid keyId from KMS");
        }

        final byte[] encryptedKey = new byte[gdkResult.getCiphertextBlob().remaining()];
        gdkResult.getCiphertextBlob().get(encryptedKey);

        final SecretKeySpec key = new SecretKeySpec(rawKey, algorithm.getDataKeyAlgo());
        //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.10
        //# The output MUST be the same as the Master Key Generate Data Key
        //# (../master-key-interface.md#generate-data-key) interface.
        return new DataKey<>(
            //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.10
            //# The response's "Plaintext" MUST be the plaintext in
            //# the output.
            key,
            //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.10
            //# The response's cipher text blob MUST be used as the
            //# returned as the ciphertext for the encrypted data key in the output.
            encryptedKey,
            gdkResultKeyId.getBytes(StandardCharsets.UTF_8),
    this
        );
    }

    //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.11
    //# The inputs MUST be the same as the Master Key Encrypt Data Key
    //# (../master-key-interface.md#encrypt-data-key) interface.
    /**
     * @see KmsMasterKey#encryptDataKey(CryptoAlgorithm, Map, DataKey)
     */
    @Override
    public DataKey encryptDataKey(final CryptoAlgorithm algorithm,
                                                           final Map encryptionContext,
                                                           final DataKey dataKey) {
        final SecretKey key = dataKey.getKey();
        /* Precondition: The key format MUST be RAW. */
        if (!key.getFormat().equals("RAW")) {
            throw new IllegalArgumentException("Only RAW encoded keys are supported");
        }

        try {
            //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.11
            //# The master
            //# key MUST use the configured AWS KMS client to make an AWS KMS Encrypt
            //# (https://docs.aws.amazon.com/kms/latest/APIReference/
            //# API_Encrypt.html) request constructed as follows:
            final EncryptResult encryptResult = kmsClient_.encrypt(updateUserAgent(
                    new EncryptRequest()
                            .withKeyId(awsKmsIdentifier_)
                            .withPlaintext(ByteBuffer.wrap(key.getEncoded()))
                            .withEncryptionContext(encryptionContext)
                            .withGrantTokens(grantTokens_)));

            final byte[] edk = new byte[encryptResult.getCiphertextBlob().remaining()];
            encryptResult.getCiphertextBlob().get(edk);
            final String encryptResultKeyId = encryptResult.getKeyId();
            //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.11
            //# The AWS KMS Encrypt response MUST contain a valid "KeyId".
            /* Postcondition: Must have an AWS KMS ARN from AWS KMS encrypt. */
            if (parseInfoFromKeyArn(encryptResultKeyId) == null) {
                throw new IllegalStateException("Received an empty or invalid keyId from KMS");
            }

            //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.11
            //# The output MUST be the same as the Master Key Encrypt Data Key
            //# (../master-key-interface.md#encrypt-data-key) interface.
            return new DataKey<>(
                dataKey.getKey(),
                //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.11
                //# The
                //# response's cipher text blob MUST be used as the "ciphertext" for the
                //# encrypted data key.
                edk,
                encryptResultKeyId.getBytes(StandardCharsets.UTF_8),
                this
            );
        } catch (final AmazonServiceException asex) {
            throw new AwsCryptoException(asex);
        }
    }

    /**
     * Will attempt to decrypt if awsKmsArnMatchForDecrypt returns true in
     * {@link AwsKmsMrkAwareMasterKey#filterEncryptedDataKeys(String, AwsKmsCmkArnInfo, EncryptedDataKey)}.
     * An extension of
     * {@link KmsMasterKey#decryptDataKey(CryptoAlgorithm, Collection, Map)}
     * but with an awareness of the properties of multi-Region keys.
     */
    @Override
    //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9
    //# The inputs MUST be the same as the Master Key Decrypt Data Key
    //# (../master-key-interface.md#decrypt-data-key) interface.
    public DataKey decryptDataKey(
            final CryptoAlgorithm algorithm,
            final Collection encryptedDataKeys,
            final Map encryptionContext
    ) throws AwsCryptoException {
        final List exceptions = new ArrayList<>();
        final String providerId = this.getProviderId();

        return encryptedDataKeys
                .stream()
                //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9
                //# The set of encrypted data keys MUST first be filtered to match this
                //# master key's configuration.
                .filter(edk -> filterEncryptedDataKeys(providerId, awsKmsIdentifier_, edk))
                //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9
                //# For each encrypted data key in the filtered set, one at a time, the
                //# master key MUST attempt to decrypt the data key.
                .map(edk -> {
                    try {
                        return decryptSingleEncryptedDataKey(
                                this,
                                kmsClient_,
                                awsKmsIdentifier_,
                                grantTokens_,
                                algorithm,
                                edk,
                                encryptionContext
                        );
                    } catch (final AmazonServiceException amazonServiceException) {
                        //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9
                        //# If this attempt
                        //# results in an error, then these errors MUST be collected.
                        exceptions.add(amazonServiceException);
                    }
                    return null;
                })
                /* Need to filter null
                 * because an Optional
                 * of a null is crazy.
                 * Therefore `findFirst` will throw
                 * if it sees `null`.
                 */
                .filter(Objects::nonNull)
                //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9
                //# If the AWS KMS response satisfies the requirements then it MUST be
                //# use and this function MUST return and not attempt to decrypt any more
                //# encrypted data keys.
                /* Order is important.
                 * Process the encrypted data keys in the order they exist in the encrypted message.
                 */
                .findFirst()
                //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9
                //# If all the input encrypted data keys have been processed then this
                //# function MUST yield an error that includes all the collected errors.
                //
                //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9
                //# The output MUST be the same as the Master Key Decrypt Data Key
                //# (../master-key-interface.md#decrypt-data-key) interface.
                /* Exceptional Postcondition: Master key was unable to decrypt. */
                .orElseThrow(() -> buildCannotDecryptDksException(exceptions));
    }

    /**
     * Pure function for decrypting and encrypted data key.
     * This is refactored out of `decryptDataKey`
     * to facilitate testing to ensure correctness.
     *
     */
    static DataKey decryptSingleEncryptedDataKey(
            final AwsKmsMrkAwareMasterKey masterKey,
            final AWSKMS client,
            final String awsKmsIdentifier,
            final List grantTokens,
            final CryptoAlgorithm algorithm,
            final EncryptedDataKey edk,
            final Map encryptionContext
    ) {

        //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9
        //# To decrypt the encrypted data key this master key MUST use the
        //# configured AWS KMS client to make an AWS KMS Decrypt
        //# (https://docs.aws.amazon.com/kms/latest/APIReference/
        //# API_Decrypt.html) request constructed as follows:
        final DecryptResult decryptResult = client.decrypt(updateUserAgent(
                new DecryptRequest()
                        .withCiphertextBlob(ByteBuffer.wrap(edk.getEncryptedDataKey()))
                        .withEncryptionContext(encryptionContext)
                        .withGrantTokens(grantTokens)
                        .withKeyId(awsKmsIdentifier)));

        final String decryptResultKeyId = decryptResult.getKeyId();
        /* Exceptional Postcondition: Must have a CMK ARN from AWS KMS to match. */
        if (decryptResultKeyId == null) {
            throw new IllegalStateException("Received an empty keyId from KMS");
        }
        //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9
        //# If the call succeeds then the response's "KeyId" MUST be equal to the
        //# configured AWS KMS key identifier otherwise the function MUST collect
        //# an error.
        if (!awsKmsIdentifier.equals(decryptResultKeyId)) {
            throw new IllegalStateException("Received an invalid response from KMS Decrypt call: Unexpected keyId.");
        }

        //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9
        //# The response's "Plaintext"'s length MUST equal the length
        //# required by the requested algorithm suite otherwise the function MUST
        //# collect an error.
        if (decryptResult.getPlaintext().limit() != algorithm.getDataKeyLength()) {
            throw new IllegalStateException("Received an unexpected number of bytes from KMS");
        }

        final byte[] rawKey = new byte[algorithm.getDataKeyLength()];
        decryptResult.getPlaintext().get(rawKey);

        return new DataKey<>(
                new SecretKeySpec(rawKey, algorithm.getDataKeyAlgo()),
                edk.getEncryptedDataKey(),
                edk.getProviderInformation(),
                masterKey);
    }

    /**
     * A pure function to filter encrypted data keys.
     * This function is refactored out from `decryptDataKey`
     * to facilitate testing and ensure correctness.
     *
     * An AWS KMS Master key should only attempt
     * to process an Encrypted Data Key
     * if the information in the Encrypted Data Key
     * matches the master keys configuration.
     *
     */
    static boolean filterEncryptedDataKeys (
        final String providerId,
        final String awsKmsIdentifier_,
        final EncryptedDataKey edk
    ) {
        final String edkKeyId = new String(edk.getProviderInformation(), StandardCharsets.UTF_8);

        final AwsKmsCmkArnInfo providerArnInfo = parseInfoFromKeyArn(edkKeyId);

        //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9
        //# Additionally each 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".
        if (providerArnInfo == null || !"key".equals(providerArnInfo.getResourceType())) {
            throw new IllegalStateException("Invalid provider info in message.");
        }

        //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9
        //# To match the encrypted data key's
        //# provider ID MUST exactly match the value "aws-kms" and the the
        //# function AWS KMS MRK Match for Decrypt (aws-kms-mrk-match-for-
        //# decrypt.md#implementation) called with the configured AWS KMS key
        //# identifier and the encrypted data key's provider info MUST return
        //# "true".
        return edk.getProviderId().equals(providerId) &&
                awsKmsArnMatchForDecrypt(awsKmsIdentifier_, edkKeyId);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy