How to Use the New AWS Encryption SDK to Simplify Data Encryption and Improve Application Availability

The AWS Cryptography team is happy to announce the AWS Encryption SDK. This new SDK makes encryption easier for developers while minimizing errors that could lessen the security of your applications. The new SDK does not require you to be an AWS customer, but it does include ready-to-use examples for AWS customers.

Developers using encryption often face two problems:

  1. How to correctly generate and use a key to encrypt data.
  2. How to protect the key after it has been used.

The library provided in the new AWS Encryption SDK addresses the first problem by transparently implementing the low-level details using the cryptographic provider available in your development environment. The library helps address the second problem by providing intuitive interfaces to let you choose how you want to protect your keys. Developers can then focus on the cores of the applications they are building, instead of on the complexities of encryption. In this blog post, I will show you how you can use the AWS Encryption SDK to simplify the process of encrypting data and how to protect your keys in ways that help improve application availability by not tying you to a single region or key management solution.

Envelope encryption and the new SDK

An important concept to understand when using the AWS Encryption SDK is envelope encryption (also known as hybrid encryption). Different algorithms have different strengths, and no single algorithm fits every use case. For example, solutions with good key management properties (such as RSA or AWS Key Management Service [KMS]) often do not work well with large amounts of data. Envelope encryption solves this problem by encrypting bulk data with a single-use data key appropriate for large amounts of data (such as AES-GCM). Envelope encryption then encrypts that data key with a master key that uses an algorithm or other solution appropriate for key management.

Another advantage of envelope encryption is that a single message can be encrypted so that multiple recipients can decrypt it. Rather than having everyone share a key (which is usually insecure) or encrypting the entire message multiple times (which is impractical), only the data key is encrypted multiple times by using each recipient’s keys. This significantly reduced amount of duplication makes encrypting with multiple keys far more practical.

The downside of envelope encryption is implementation complexity. All clients must be able to generate and parse the data formats, handle multiple keys and algorithms, and ideally remain reasonably forward and backward compatible.

How does the AWS Encryption SDK help me?

The AWS Encryption SDK comes with a carefully designed and reviewed data format that supports multiple secure algorithm combinations (with room for future expansion) and has no limits on the types or algorithms of the master keys. The AWS Encryption SDK itself is a production‑ready reference Java implementation with direct support for KMS and the Java Cryptography Architecture (JCA/JCE), which includes support for AWS CloudHSM and other PKCS #11 devices. Implementations of this SDK in other languages are currently being developed.

One benefit of the AWS Encryption SDK is that it takes care of the low-level cryptographic details so that you can focus on moving data. Next, I will show how little code you need to build a powerful and secure multiregion solution.

Example 1: Encrypting application secrets under multiple regional KMS master keys for high availability

Many customers want to build systems that not only span multiple Availability Zones, but also multiple regions. Such deployment can be challenging when data is encrypted with KMS because you cannot share KMS customer master keys (CMKs) across regions. With envelope encryption, you can work around this limitation by encrypting the data key with multiple KMS CMKs in different regions. Applications running in each region can use the local KMS endpoint to decrypt the ciphertext for faster and more reliable access.

For all examples, I will assume that I am running on Amazon EC2 instances configured with IAM roles for EC2. This lets us avoid credential management and take advantage of built-in logic that routes requests to the nearest endpoints. These examples also assume that the AWS SDK for Java (different than the AWS Encryption SDK) and Bouncy Castle are available.

The encryption logic has a very simple high-level design. After reading in some parameters from the command line, I get the master keys and use them to encrypt the file (as shown in the following code example). I will provide the missing methods later in this post.

public static void main(final String[] args) throws Exception {
    // Get parameters from the command line
    final String inputFile = args[0];
    final String outputFile = args[1];

    // Get all the master keys we'll use
    final MasterKeyProvider<?> provider = getMasterKeyProvider();

    // Actually encrypt the data
    encryptFile(provider, inputFile, outputFile);
}

Create master keys and combine them into a single master key provider

The following code example shows how you can encrypt data under CMKs in three US regions: us-east-1, us-west-1, and us-west-2. The example assumes that you have already set up the CMKs and created aliases named alias/exampleKey in each region for each CMK. For more information about creating CMKs and aliases, see Creating Keys in the AWS KMS documentation.

This example then uses the MultipleProviderFactory to combine all the master keys into a single master key provider. Note that the first master key is the one used to generate the new data key and the other master keys are used to encrypt the new data key.

private static MasterKeyProvider<?> getMasterKeyProvider() {
    // Get credentials from Roles for EC2
    final AWSCredentialsProvider creds = 
        new InstanceProfileCredentialsProvider();

    // Get KmsMasterKeys for the regions we care about
    final String aliasName = "alias/exampleKey";
    final KmsMasterKey useast1 = KmsMasterKey.getInstance(creds,
                "arn:aws:kms:us-east-1:" + ACCOUNT_ID + ":" + aliasName);
    final KmsMasterKey uswest1 = KmsMasterKey.getInstance(creds,
                "arn:aws:kms:us-west-1:" + ACCOUNT_ID + ":" + aliasName);
    final KmsMasterKey uswest2 = KmsMasterKey.getInstance(creds,
                "arn:aws:kms:us-west-2:" + ACCOUNT_ID + ":" + aliasName);

    return MultipleProviderFactory.buildMultiProvider(
        useast1, uswest1, uswest2);
}

The logic to construct a MasterKeyProvider could easily be built once by your central security team and then reused across your company to both simplify development and ensure that all encrypted data meets corporate standards.

Encrypt the data

The data you encrypt can come from anywhere and be distributed however you like. In the following code example, I am reading a file from disk and writing out an encrypted copy. The AWS Encryption SDK integrates directly with Java’s streams to make this easy.

private static void encryptFile(final MasterKeyProvider<?> provider,
        final String inputFile, final String outputFile) throws IOException {
    // Get an instance of the encryption logic
    final AwsCrypto crypto = new AwsCrypto();

    // Open the files for reading and writing
    try (
            final FileInputStream in = new FileInputStream(inputFile);
            final FileOutputStream out = new FileOutputStream(outputFile);
            // Wrap the output stream in encryption logic
            // It is important that this is closed because it adds footers
            final CryptoOutputStream<?> encryptingStream =
                    crypto.createEncryptingStream(provider, out)) {
        // Copy the data over
        IOUtils.copy(in, encryptingStream);
    }
}

This file could contain, for example, secret application configuration data (such as passwords, certificates, and the like) that is then sent to EC2 instances as EC2 user data upon launch.

Decrypt the data

The following code example decrypts the contents of the EC2 user data and writes it to the specified file. The AWS SDK for Java defaults to using KMS in the local region, so decryption proceeds quickly without cross-region calls.

public static void main(String[] args) throws Exception {
    // Get parameters from the command line
    final String outputFile = args[0];

    // Create a master key provider that points to the local
    // KMS stack and uses Roles for EC2 to get credentials.
    final KmsMasterKeyProvider provider = new KmsMasterKeyProvider(
        new InstanceProfileCredentialsProvider());

    // Get an instance of the encryption logic
    final AwsCrypto crypto = new AwsCrypto();

    // Open a stream to read the user data
    // and a stream to write out the decrypted file
    final URL userDataUrl = new URL("http://169.254.169.254/latest/user-data");
    try (
            final InputStream in = userDataUrl.openStream();
            final FileOutputStream out = new FileOutputStream(outputFile);
            // Wrap the input stream in decryption logic
            final CryptoInputStream<?> decryptingStream =
                    crypto.createDecryptingStream(provider, in)) {
        // Copy the data over
        IOUtils.copy(decryptingStream, out);
    }
}

Congratulations! You have just encrypted data under master keys in multiple regions and have code that will always decrypt the data by using the local KMS stack. This gives you higher availability and lower latency for decryption, while still only needing to manage a single ciphertext.

Example 2: Encrypting application secrets under master keys from different providers for escrow and portability

Another reason why you might want to encrypt data under multiple master keys is to avoid relying on a single provider for your keys. By not tying yourself to a single key management solution, you help improve your applications’ availability. This approach also might help if you have compliance, data loss prevention, or disaster recovery requirements that require multiple providers.

You can use the same technique demonstrated previously in this post to encrypt your data to an escrow or additional decryption master key that is independent from your primary provider. This example demonstrates how to use an additional master key, which is an RSA public key with the private key stored in a key management infrastructure independent from KMS such as an offline Hardware Security Module (HSM). (Creating and managing the RSA key pair are out of scope for this blog.)

Encrypt the data with a public master key

Just like the previous code example that created a number of KmsMasterKeys to encrypt data, the following code example creates one more MasterKey for use with the RSA public key. The example uses a JceMasterKey because it uses a java.security.PublicKey object from the Java Cryptography Extensions (JCE). The example then passes the new MasterKey into the MultipleProviderFactory (along with all the other master keys). The example loads the public key from a file called rsa_public_key.der.

private static MasterKeyProvider<?> getMasterKeyProvider()
      throws IOException, GeneralSecurityException {
    // Get credentials from Roles for EC2
    final AWSCredentialsProvider creds =
        new InstanceProfileCredentialsProvider();

    // Get KmsMasterKeys for the regions we care about
    final String aliasName = "alias/exampleKey";
    final KmsMasterKey useast1 = KmsMasterKey.getInstance(creds,
            "arn:aws:kms:us-east-1:" + ACCOUNT_ID + ":" + aliasName);
    final KmsMasterKey uswest1 = KmsMasterKey.getInstance(creds,
            "arn:aws:kms:us-west-1:" + ACCOUNT_ID + ":" + aliasName);
    final KmsMasterKey uswest2 = KmsMasterKey.getInstance(creds,
            "arn:aws:kms:us-west-2:" + ACCOUNT_ID + ":" + aliasName);

    // Load the RSA public key from a file and make a MasterKey from it.
    final byte[] rsaBytes = Files.readAllBytes(
        new File("rsa_public_key.der").toPath());
    final KeyFactory rsaFactory = KeyFactory.getInstance("RSA");
    final PublicKey rsaKey = rsaFactory.generatePublic(
        new X509EncodedKeySpec(rsaBytes));
    final JceMasterKey rsaMasterKey =
        JceMasterKey.getInstance(rsaKey, null,
            "escrow-provider", "escrow",
            "RSA/ECB/OAEPWithSHA-256AndMGF1Padding");

    return MultipleProviderFactory.buildMultiProvider(
        useast1, uswest1, uswest2, rsaMasterKey);
}

Decrypt the data with the private key

Many HSMs support the standard Java KeyStore interface or at least supply PKCS #11 drivers, which allow you to use the existing Java KeyStore implementations. The following decryption code example uses an RSA private key from a KeyStore.

public static void main(String[] args) throws Exception {
    // Get parameters from the command line
    final String inputFile = args[0];
    final String outputFile = args[1];

    // Get the KeyStore
    // In a production system, this would likely be backed by an HSM.
    // For this example, it will simply be a file on disk
    final char[] keystorePassword = "example".toCharArray();
    final KeyStore keyStore = KeyStore.getInstance("JKS");
    try (final FileInputStream fis = new FileInputStream("cryptosdk.jks")) {
        keyStore.load(fis, keystorePassword);
    }

    // Create a master key provider from the keystore.
    // Be aware that because KMS isn’t being used, it cannot directly
    // protect the integrity or authenticity of this data.
    final KeyStoreProvider provider = new KeyStoreProvider(keyStore,
            new PasswordProtection(keystorePassword),
            "escrow-provider", "RSA/ECB/OAEPWithSHA-256AndMGF1Padding");

    // Get an instance of the encryption logic
    final AwsCrypto crypto = new AwsCrypto();

    // Open a stream to read the encrypted file
    // and a stream to write out the decrypted file
    try (
            final FileInputStream in = new FileInputStream(inputFile);
            final FileOutputStream out = new FileOutputStream(outputFile);
            // Wrap the output stream in encryption logic
            final CryptoInputStream<?> decryptingStream =
                    crypto.createDecryptingStream(provider, in)) {
        // Copy the data over
        IOUtils.copy(decryptingStream, out);
    }
}

Conclusion

Envelope encryption is powerful, but traditionally, it has been challenging to implement. The new AWS Encryption SDK helps manage data keys for you, and it simplifies the process of encrypting data under multiple master keys. As a result, this new SDK allows you to focus on the code that drives your business forward. It also provides a framework you can easily extend to ensure that you have a cryptographic library that is configured to match and enforce your standards.

We are excited about releasing the AWS Encryption SDK and can’t wait to hear what you do with it. If you have comments about the new SDK or anything in this blog post, please add a comment in the “Comments” section below. If you have implementation or usage questions, start a new thread on the KMS forum.

- Greg

Comments