AWS: Envelop Encryption & Decryption using AWS Key Management Service (Envelop encryption)

As per Amazon, AWS Key Management Service (AWS KMS) is a managed service that makes it easy for you to create and control the encryption keys used to encrypt your data. The master keys that you create in AWS KMS are protected by FIPS 140-2 validated cryptographic modules.

The primary resources in AWS KMS are customer master keys (CMKs). You can use a CMK to encrypt and decrypt up to 4 KB (4096 bytes) of data. Typically, you use CMKs to generate, encrypt, and decrypt the data keys that you use outside of AWS KMS to encrypt your data. This strategy is known as envelope encryption.

Encryption process

  • Generate a data key using the GenerateDataKey operation of AWS KMS. The operation returns a plaintext copy of the data key and a copy of the data key encrypted using the master key (CMK)
  • Encrypt the data with the plain data key and destroy the key
  • We can store this encrypted key along with our encrypted message.The message can’t be decrypted unless you can decrypt the data key and the data key can’t be decrypted unless you have access to the master key.

Decryption process

  • Decrypt the encrypted key using our master key to get the plain data key
  • Decrypt the data with the plain data key
You can read the KMS concepts here.

Following is the helper class for Encrypt or Decrypt the data with KMS key in .NET/.NET Core with C#. AWSSDK.KeyManagementService  is the nuget package required to work with AWS KMS.

using Amazon.KeyManagementService;
using Amazon.KeyManagementService.Model;
using Amazon.Runtime;
using log4net;
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;


public class KMSHelper
{
    private AmazonKeyManagementServiceClient client;
    private static readonly ILog _logger = LogManager.GetLogger(typeof(KMSHelper));
    private readonly string accessKey, secretKey, serviceUrl;

    public KMSHelper(string accessKey, string secretKey, string serviceUrl)
    {
        this.accessKey = accessKey;
        this.secretKey = secretKey;
        this.serviceUrl = serviceUrl;
        client = GetClient();
    }

    private AmazonKeyManagementServiceClient GetClient()
    {
        if (client == null)
        {
            try
            {
                // DynamoDB config object
                AmazonKeyManagementServiceConfig clientConfig = new AmazonKeyManagementServiceConfig
                {
                    // Set the endpoint URL
                    ServiceURL = serviceUrl
                };
                client = new AmazonKeyManagementServiceClient(accessKey, secretKey, clientConfig);
            }
            catch (AmazonKeyManagementServiceException ex)
            { _logger.Error($"Error (AmazonKeyManagementServiceException) creating KMS client", ex); }
            catch (AmazonServiceException ex)
            { _logger.Error($"Error (AmazonServiceException) creating KMS client", ex); }
            catch (Exception ex)
            { _logger.Error($"Error creating KMS client", ex); }
        }
        return client;
    }

    /// <summary>
    /// Generates new data key under the master key
    /// </summary>
    /// <param name="masterKeyId">Master key</param>
    /// <returns></returns>
    private async Task<MemoryStream> GenerateDataKey(string masterKeyId)
    {
        using (var kmsClient = GetClient())
        {
            var result = await kmsClient.GenerateDataKeyAsync(new GenerateDataKeyRequest
            {
                KeyId = masterKeyId,
                KeySpec = DataKeySpec.AES_256
            });

            return result.CiphertextBlob;
        }
    }

    /// <summary>
    /// Decrypts the data key using the master key
    /// </summary>
    /// <param name="ciphertext">Stream to decrypt</param>
    /// <returns></returns>
    private async Task<MemoryStream> DecryptDataKey(MemoryStream ciphertext)
    {
        using (var kmsClient = GetClient())
        {
            var decryptionResponse = await kmsClient.DecryptAsync(new DecryptRequest
            {
                CiphertextBlob = ciphertext
            });

            return decryptionResponse.Plaintext;
        }
    }

    /// <summary>
    /// Encrypts the data key using the master key
    /// </summary>
    /// <param name="masterKeyId">Master key arn</param>
    /// <param name="plaintext">Plain data key</param>
    /// <returns></returns>
    private async Task<string> EncryptDataKey(string masterKeyId, MemoryStream plaintext)
    {
        using (var kmsClient = new AmazonKeyManagementServiceClient(accessKey, secretKey, serviceUrl))
        {
            var encryptionResponse = await kmsClient.EncryptAsync(new EncryptRequest
            {
                KeyId = masterKeyId,
                Plaintext = plaintext
            });

            return Convert.ToBase64String(encryptionResponse.CiphertextBlob.ToArray());
        }
    }

    /// <summary>
    /// Encrypts the text using the master key
    /// </summary>
    /// <param name="textToEncrypt">Text to encrypt</param>
    /// <param name="masterKeyId">Master key arn</param>
    /// <returns></returns>
    public async Task<string> Encrypt(byte[] textToEncrypt, string masterKeyId)
    {
        var kmsClient = new AmazonKeyManagementServiceClient(accessKey, secretKey, serviceUrl);

        using (var algorithm = Aes.Create())
        {
            // Create the streams used for encryption.
            using (MemoryStream msEncrypt = new MemoryStream())
            {
                // Generates the data key under the master key
                var dataKey = await kmsClient.GenerateDataKeyAsync(new GenerateDataKeyRequest
                {
                    KeyId = masterKeyId,
                    KeySpec = DataKeySpec.AES_256
                });

                msEncrypt.WriteByte((byte)dataKey.CiphertextBlob.Length);
                dataKey.CiphertextBlob.CopyTo(msEncrypt);
                algorithm.Key = dataKey.Plaintext.ToArray();

                // Writing algorithm.IV in output stream for decryption purpose.
                msEncrypt.Write(algorithm.IV, 0, algorithm.IV.Length);

                // Create a decrytor to perform the stream transform.
                ICryptoTransform encryptor = algorithm.CreateEncryptor(algorithm.Key, algorithm.IV);

                using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
                {
                    using (MemoryStream input = new MemoryStream(textToEncrypt))
                    {
                        input.CopyTo(csEncrypt);
                        csEncrypt.FlushFinalBlock();
                    }
                    return Convert.ToBase64String(msEncrypt.ToArray());
                }
            }
        }
    }

    /// <summary>
    /// Decrypts the text encrypted with the master key
    /// </summary>
    /// <param name="textToDecrypt">Encrypted text to decrypt</param>
    /// <returns></returns>
    public async Task<string> Decrypt(byte[] textToDecrypt)
    {
        // Create the streams used for decryption.
        using (MemoryStream msDecrypt = new MemoryStream(textToDecrypt))
        {
            var length = msDecrypt.ReadByte();
            var buffer = new byte[length];
            msDecrypt.Read(buffer, 0, length);

            // Decrypt the datakey
            MemoryStream dataKeyCipher = await DecryptDataKey(new MemoryStream(buffer));

            using (var algorithm = Aes.Create())
            {
                algorithm.Key = dataKeyCipher.ToArray();

                var iv = algorithm.IV;
                msDecrypt.Read(iv, 0, iv.Length);
                algorithm.IV = iv;

                // Create a decrytor to perform the stream transform.
                ICryptoTransform decryptor = algorithm.CreateDecryptor(algorithm.Key, algorithm.IV);

                using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
                {
                    using (MemoryStream srDecrypt = new MemoryStream())
                    {
                        csDecrypt.CopyTo(srDecrypt);

                        //Write all data to the stream.
                        return Encoding.ASCII.GetString(srDecrypt.ToArray());
                    }
                }
            }
        }
    }
}
Happy Coding!😊

Gopikrishna

    Blogger Comment
    Facebook Comment

7 comments:

  1. I'm getting an invalid ciphertext. It was encrypted by Connect. Thoughts?

    ReplyDelete
    Replies
    1. The ciphertext given to the decryption process should be generated using the encryption method in the above helper method only. Because in the encryption process we are storing the masterkey cipher text blob in the generated cipher text.

      Delete
  2. Hi ,

    I am getting exception in the following line in Encrypt method .
    var dataKey = await kmsClient.GenerateDataKeyAsync(new GenerateDataKeyRequest
    {
    KeyId = masterKeyId,
    KeySpec = DataKeySpec.AES_256
    });
    There is no error message displayed. KeyID i am passing ARN.

    Regards
    Arun

    ReplyDelete
  3. I'm getting CryptographicException: The input data is not a complete block. Any thoughts to this issue

    ReplyDelete
  4. I also encountering the same issue as the user above, I am amble to encrypt the data but when i pass the same string to the decrypt data method, I get invalidciphertext error

    ReplyDelete
    Replies
    1. While passing it to decrypt method you should do this
      Decrypt(Convert.FromBase64String(encryptedText))

      Delete