Category Archives: Amazon Web Services Programming

AWS Key Management System (KMS) to Encrypt and Decrypt Using the ASW Java 2 SDK

In this tutorial we explore the AWS Key Management System (KMS) to encrypt and decrypt data via the AWS Java 2 SDK. This tutorial encrypts/decrypts two different ways. We first encrypt and decrypt data directly using an AWS customer managed key (CMK). We then encrypt and decrypt the data using a data key that was generated by the AWS CMK. Of the two, the second is more secure, and the preferred way to encrypt data. Although a CMK can encrypt and decrypt data, a better practice is to use the CMK to generate data keys, which are in turn used with the relevant data. However, for the purpose of demonstration, we also use the CMK directly.

Introduction

AWS SDK for Java Version 2.x

In this tutorial we use the Java Version 2.x of the AWS SDK. The SDK provides a convenient wrapper around the AWS services’ lower-level REST calls. Be certain you use version 2.x and not 1.x, as 2.x is a rewrite of the API and so there are considerable differences between the two API versions.

Key Management Service (KMS)

AWS KMS is a service that enables generating, storing, and managing symmetric keys. The service is integrated with other Amazon offerings such as S3. Actually, most AWS services are integrated with KMS, as this list of over 50 services illustrates. However, KMS can also be used to generate and manage your own application’s keys even if that application is independent of other AWS services.

A symmetric key is a single key used to encrypt/decrypt data. This is in contrast to an asymmetric key, where a private key and public key encrypt/decrypt data. Wikipedia has a good general introduction to key encryption: Key (Cryptography). A typical strategy for symmetric key encryption is as follows. A single master key is used to encrypt/decrypt data encryption keys. These data keys encrypt/decrypt your application’s data. To ensure security, when not in use, the data keys are encrypted/decrypted by the master key. The master key is then stored in a safe location so it can be used as needed.

AWS KMS provides a secure location to store and manage your master keys. CMKs cannot be exported from KMS and can only be used by users with appropriate permissions assigned. The KMS FAQ summarizes KMS.

AWS KMS is a managed service that enables you to easily encrypt your data. AWS KMS provides a highly available key storage, management, and auditing solution for you to encrypt data within your own applications and control the encryption of stored data across AWS services.

AWS Key Management Service FAQs

AWS KMS offers many benefits for developers using AWS services.

If you are a developer who needs to encrypt data in your applications, you should use the AWS Encryption SDK with AWS KMS support to easily use and protect encryption keys. If you’re an IT administrator looking for a scalable key management infrastructure to support your developers and their growing number of applications, you should use AWS KMS to reduce your licensing costs and operational burden. If you’re responsible for proving data security for regulatory or compliance purposes, you should use AWS KMS to verify that data is encrypted consistently across the applications where it is used and stored.

AWS Key Management Service FAQs

AWS KMS offers an integrated cloud environment for managing keys.

You can perform the following key management functions in AWS KMS:


Create keys with a unique alias and description
Import your own key material
Define which IAM users and roles can manage keys
Define which IAM users and roles can use keys to encrypt and decrypt data
Choose to have AWS KMS automatically rotate your keys on an annual basis
Temporarily disable keys so they cannot be used by anyone
Re-enable disabled keys
Delete keys that you no longer use
Audit use of keys by inspecting logs in AWS CloudTrail
Create custom key stores*
Connect and disconnect custom key stores*
Delete custom key stores*

* The use of custom key stores requires CloudHSM resources to be available in your account.

AWS KMS FAQ.

For more introductory information refer to The AWS Key Management Features webpage maintained by Amazon. Also refer to the videos embedded at the end of this tutorial.

In this tutorial we perform the following tasks:

  • create two users, one to manage a CMK and another to use the CMK,
  • create a CMK and assign the users to the key,
  • create an application that uses the CMK directly to encrypt/decrypt data,
  • discuss why using the CMK directly is not an optimal encryption strategy,
  • and create an application that uses the CMK to create a data key to encrypt/decrypt data.

In this tutorial we limit using KMS to generating a data key and to encrypting/decrypting data. You can also manage keys through the Java SDK; however, this tutorial does not cover key management, assuming instead you will do so through the AWS console or AWS Command-line Interface (CLI).

  • It is assumed you have an AWS account, know your way around the AWS Console, and have enough experience with Java programming that you do not require help using an IDE such as Eclipse.

Creating the CMK

Before using the CMK we need to create it. Although you can use the Java SDK to perform all the following tasks, we use the AWS Console for creating the required users and key.

Create Users

We need to create users for our CMK. The first user we create is the key manager. Although this tutorial does not subsequently use this user, we include it as in a real project you would eventually need this user for managing keys. The second user we create is the key user. This is the user that is allowed to use the CMK to encrypt/decrypt data. We do use this data in the Java application.

  • All Users, Keys, and potentially sensitive information will have been removed from my account before this tutorial is posted.

Create Manager

Let’s first create the manager user.

  • Navigate to IAM, Users, and add a User named KmsKeyManager.
  • Assign the user programmatic and console access.
  • Create a password for the user and uncheck the Require password reset checkbox.
Adding the KmsKeyManager user via the Add user page

  • When you add the user to groups, create a new group named KmsKeyTutorialGroup and assign it AdministratorAccess.
Adding policies to a KmsKeyTutorialGroup
  • Add KmsKeyManager to KmsKeyTutorialGroup.
Assing KmsKeyManager to KmsKeyTutorialGroup
  • After creating the user you should see a screen similar to the following. Do not forget to download the access keys so you can use them in your Java program.
KmsKeyManager created successfully

Create Encrypt/Decrypt User

Let’s now create the user we use to encrypt/decrypt data in our Java application.

  • Create a user named KmsTutorialKeyUser and assign programmatic access.
Creating the KmsTutorialKeyUser
  • Do not assign KmsTutorialKeyUser to any groups.
The Users screen with two newly added users, KmsKeyManager and KMSTutorialKeyUser

Create KMS Key

  • Navigate to IAM and then select Encryption keys to bring you to the Key Management Service (KMS).
Customer managed keys screen with no keys
  • Create a new key with the alias, KmsTutorialKey.
  • Select KMS as the Key material origin.
Creating the KmsTutorialKey

  • Assign KmsKeyManager as the key administrator.
Key administration permissions assigned to KmsKeyManager
  • Assign KmsTutorialKeyUser as the key user (can encrypt and decrypt using the key).
Assigning encrypt/decrypt permission to KmsTutorialKeyUser
  • If interested, review the JSON document.
Complete policy document is JSON
  • After finishing, you should see a screen similar to the following.
KMS screen with newly created CMK

Java Project

Let’s create the Java project using Maven. Although I use Eclipse, any IDE or the command-line should work. It is assumed you can create a Java project that uses Maven to build. If you need help accomplishing this task, refer to a tutorial online. The following is a good introductory tutorial for Maven and Eclipse.

Project Setup

  • Create a new Java project that uses Maven to build. Use the following POM.
<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>com.tutorial.aws</groupId>
	<artifactId>KMSTutorial</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<properties>
		<java.version>1.8</java.version>
		<maven.compiler.source>1.8</maven.compiler.source>
		<maven.compiler.target>1.8</maven.compiler.target>
	</properties>
	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>software.amazon.awssdk</groupId>
				<artifactId>bom</artifactId>
				<version>2.5.25</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>
	<dependencies>
		<dependency>
			<groupId>software.amazon.awssdk</groupId>
			<artifactId>kms</artifactId>
		</dependency>
		<dependency>
			<artifactId>auth</artifactId>
			<groupId>software.amazon.awssdk</groupId>
		</dependency>
		<dependency>
			<artifactId>aws-core</artifactId>
			<groupId>software.amazon.awssdk</groupId>
		</dependency>
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-api</artifactId>
			<version>1.7.5</version>
		</dependency>
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-simple</artifactId>
			<version>1.6.4</version>
		</dependency>
	</dependencies>
</project>

Note the POM includes the following lines. You might not require these lines; however, the Java 2 SDK uses features that require Java 8 or higher, and I could only get the code to compile including these lines. YMMV.

<properties>
  <java.version>1.8</java.version>
  <maven.compiler.source>1.8</maven.compiler.source>
  <maven.compiler.target>1.8</maven.compiler.target>
</properties>

The POM adds the AWS Bill of Materials (BOM) to free us from having to manage the library versions, focusing only on including the correct dependencies rather than their versions. The POM also includes the KMS library and the core libraries required by AWS.

  • If using Eclipse, add a file named observation.json to the resource folder.
  • The observation.json file is a simple JSON record.
{
      "stationid": 221,
      "date": "1992-03-12",
      "time": "091312",
      "message":"This is a secret message. Please encrypt it when storing on disk."
}

KmsClient

The AWS SDK is consistent in how you interact with AWS’s different services. The 2.x API version consistently follows the Fluent Interface/Builder pattern. You can find more information on this pattern if interested by starting with the wikipedia page. A good introductory explanation is found in the following blog post: Another builder pattern for Java. Rather than instantiating new instances of a class, you build the class you a builder. When using the AWS SDK, you create a client with the required credentials using the client’s associated builder. For instance a KmsClient has a KmsClient.Builder that builds it. Different services have different clients. The KMS service uses the KmsClient. Clients work with AWS via requests and is returned responses. The KmsClient class, for example, uses requests to encrypt/decrypt, create keys, and manage keys.

Let’s create the KmsClient. But first we need to return to the AWS Console and copy the CMK key’s Amazon Resource Name (ARN). The ARN is how our application’s client will know where to access the CMK in KMS.

  • Navigate to the key and copy the key’s ARN.
Copy the CMK ARN in the AWS Console

An ARN identifies any resource on AWS uniquely. The client uses this to access the CMK.

  • Create a new class in named KMSExample in the com.tutorial.aws.kms package.
  • Although it is not recommended you hardcode keys in your code, for convenience we create the key and secretKey static variables to held the KmsTutorialKeyUser key and secretKey.
  • Create a keyArn static variable and assign it the ARN you copied above from the AWS console.
  • Create a KmsClient variable and build it in the constructor for KMSExample.
  • In main, create a KMSExample instance.

package com.tutorial.aws.kms;

import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.kms.KmsClient;

public class KMSExample {

  final static String key = "<key_value_here>";
  final static String secretKey = "<secret_key_value_here";
  final static String keyArn = "<key_arn_here>";

  public KmsClient kmsClient;
  
  public KMSExample() {
    AwsBasicCredentials awsCreds = AwsBasicCredentials.create(key,
      secretKey);
    this.kmsClient = KmsClient.builder()
       .credentialsProvider(StaticCredentialsProvider.create(awsCreds))
       .region(Region.US_EAST_1).build();
  }
  
  public static void main(String[] args) {
    try {
      KMSExample kmsExample = new KMSExample();
    } 
    catch (Exception e) {
      e.printStackTrace();
    }
  }	
}
  • Build the application and run the application, just to ensure everything works.

The KMSExample class uses a KmsClientBuilder to build a KmsClient instance. The KmsClientBuilder uses the region, the credentials, and the key’s ARN to create a KmsClient that can interact with our CMK (KmsTutorialKey).

Note that in this tutorial we create the credentials in a rather un-secure manner. We hardcode them and used StaticCredentialsProvider and AwsBasicCredentials. In a production ready application you should use the AWS Security Token Service (AWS STS) to grant temporary credentials when running the application. For more information, refer to the ASW Security Token Service documentation. Also, you might refer to the following AWS Security Blog post: Guidelines for protecting your AWS account while using programmatic access.

  • After becoming familiar with the 2.x API version, translating constructors and methods to the 2.x API’s builder methods becomes intuitive.

Encrypting and Decrypting Using the Customer Key

In the first example we encrypt and decrypt the data directly using the KmsTutorialKey. As discussed earlier, this is not the recommended way to encrypt/decrypt your application’s data. However, we include it here, as a CMK master key can be used directly in your application to encrypt/decrypt data. And, there might be situations where it’s appropriate to use the CMK directly.

Encrypt

Let’s encrypt the data. We do this by building an EncryptRequest using an EncryptRequest.Builder. The builder takes the request, the key’s ARN, and builds the EncryptRequest. We then pass the request to the KmsClient.

  • Add a method named encrypt to KMSTutorial that takes SdkBytes and returns SdkBytes.
  • Create an EncryptRequest by specifying the key’s ARN and the string to encrypt.
  • Have kmsClient encrypt the request and assign the response to an EncryptResponse.
  • Return the response’s data as SdkBytes.
public SdkBytes encrypt(SdkBytes jsonString) {
  EncryptRequest encryptRequest = EncryptRequest.builder()
    .keyId(keyArn).plaintext(jsonString).build();
  EncryptResponse encryptResponse = this.kmsClient
    .encrypt(encryptRequest);
  return encryptResponse.ciphertextBlob();
}

The EncryptRequest returns an EncryptResponse. We use the ciphertextBlob method to extract the encrypted data from the response. Note that this data is Base64 encoded when accessing it through the HTTP API as we do in this tutorial (remember the Java SDK is a wrapper around AWS Rest APIs and Rest is typically – but not always – HTTP/HTTPS).

The SdkBytes class is Amazon’s wrapper around bytes. It can be created from byte arrays, a ByteBuffer, InputStream, or a String. The AWS SDK consistently uses this class rather than the classes the SdkBytes wraps. In the preceding code we used the cipherTextBlob to obtain the encrypted data from the response to our request to encrypt data using the CMK. The cipherTextBlob returns an SdkBytes.

  • Add a method named writeToFile that takes the SdkBytes to write and the path to the file to write the data to.
public static void writeToFile(SdkBytes bytesToWrite, String path ) throws 
  IOException {
  FileChannel fc;
  FileOutputStream outputStream = new FileOutputStream(path);
  fc = outputStream.getChannel();
  fc.write(bytesToWrite.asByteBuffer());
  outputStream.close();
  fc.close();
}

The writeToFile method writes the encrypted data to a file using the Java NIO API standard in the JDK. We use the SDKBytes asByteBuffer method to convert the data to a ByteBuffer so that the FileChannel can write the data to a file.

  • Modify main to open the observation.json file as an InputStream.
  • Create the input SdkBytes from the InputStream.
  • Call the encrypt method and assign the returned SdkBytes to a variable.
  • Save the SdkBytes to a file using the writeToFile method.
public static void main(String[] args) {
  try 
  {
    KMSExample kmsExample = new KMSExample();
    InputStream in = kmsExample.getClass().getClassLoader()
      .getResourceAsStream("observation.json");
    SdkBytes inputBytes = SdkBytes.fromInputStream(in);
    SdkBytes outputBytes = kmsExample.encrypt(inputBytes);
    String path = Paths.get(".").toAbsolutePath().normalize().toString() + 
      "/observation_encrypt.json";
    KMSExample.writeToFile(outputBytes, path);
  }
  catch (Exception e) {
    e.printStackTrace();
  }
}

  • Build and run the application. You should see a new file named observation_encrypted.json. The content is binary and encrypted.

Decrypt

Let’s decrypt the encrypted data using the CMK used to encrypt the data.

  • Add a method named deCrypt to KMSTutorial that takes SdkBytes and returns SdkBytes.
  • Create a DecryptRequest by assigning it the encrypted text.
  • Have the KmsClient decrypt the request and assign the results to a DecryptResponse.
  • Return the response text.
public SdkBytes deCrypt(SdkBytes encryptedJsonString) {
  DecryptRequest decryptRequest = DecryptRequest.builder()
    .ciphertextBlob(encryptedJsonString).build();
  DecryptResponse decryptResponse = this.kmsClient
    .decrypt(decryptRequest);
  return decryptResponse.plaintext();
}

A DecryptRequest uses a DecryptRequest.Builder to build itself. The builder takes the encrypted text and sends it to the KmsClient. Note that it does not require passing the key’s ARN to decrypt.

  • Modify main to decrypt the encrypted file and print the results to the console.
public static void main(String[] args) {
  try 
  {
    KMSExample kmsExample = new KMSExample();
    InputStream in = kmsExample.getClass().getClassLoader()
      .getResourceAsStream("observation.json");
    SdkBytes inputBytes = SdkBytes.fromInputStream(in);
    SdkBytes outputBytes = kmsExample.encrypt(inputBytes);
    String path = Paths.get(".").toAbsolutePath().normalize().toString() + 
      "/observation_encrypt.json";
    KMSExample.writeToFile(outputBytes, path);
    SdkBytes output2Bytes = kmsExample.deCrypt(KMSExample
      .readFromFile(path));

    System.out.println(output2Bytes.asUtf8String());
  }
  catch (Exception e) {
    e.printStackTrace();
  }
}
  • Build and run the application and you should see the decrypted content, the same as the original file’s content, printed to the console.

Complete KMSExample

Follows is the complete source for the KMSExample class.

package com.tutorial.aws.kms;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.channels.FileChannel;
import java.nio.file.Paths;

import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.core.SdkBytes;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.kms.KmsClient;
import software.amazon.awssdk.services.kms.model.DecryptRequest;
import software.amazon.awssdk.services.kms.model.DecryptResponse;
import software.amazon.awssdk.services.kms.model.EncryptRequest;
import software.amazon.awssdk.services.kms.model.EncryptResponse;

public class KMSExample {

  final static String key = "<key_value_here>";
  final static String secretKey = "<secret_key_value_here";
  final static String keyArn = "<key_arn_here>";

  public KmsClient kmsClient;
  
  public KMSExample() {
    AwsBasicCredentials awsCreds = AwsBasicCredentials.create(key,
      secretKey);
    this.kmsClient = KmsClient.builder()
       .credentialsProvider(StaticCredentialsProvider.create(awsCreds))
       .region(Region.US_EAST_1).build();
  }

  public SdkBytes encrypt(SdkBytes jsonString) {
    EncryptRequest encryptRequest = 
    EncryptRequest.builder().keyId(keyArn).plaintext(jsonString).build();
    EncryptResponse encryptResponse = this.kmsClient
      .encrypt(encryptRequest);
    return encryptResponse.ciphertextBlob();
  }
	
  public SdkBytes deCrypt(SdkBytes encryptedJsonString) {
    DecryptRequest decryptRequest = DecryptRequest.builder()
      .ciphertextBlob(encryptedJsonString).build();
    DecryptResponse decryptResponse = this.kmsClient
      .decrypt(decryptRequest);
    return decryptResponse.plaintext();
  
  public static void main(String[] args) {
    try 
    {
      KMSExample kmsExample = new KMSExample();
      InputStream in = kmsExample.getClass().getClassLoader()
        .getResourceAsStream("observation.json");
      SdkBytes inputBytes = SdkBytes.fromInputStream(in);
      SdkBytes outputBytes = kmsExample.encrypt(inputBytes);
      String path = Paths.get(".").toAbsolutePath().normalize()
        .toString() + "/observation_encrypt.json";
      KMSExample.writeToFile(outputBytes, path);
      SdkBytes output2Bytes = kmsExample.deCrypt(KMSExample
        .readFromFile(path));
      System.out.println(output2Bytes.asUtf8String());
    }
    catch (Exception e) {
      e.printStackTrace();
    }
  }

  public static void writeToFile(SdkBytes bytesToWrite, 
    String path) throws IOException {

    FileChannel fc;
    FileOutputStream outputStream = new FileOutputStream(path);
    fc = outputStream.getChannel();
    fc.write(bytesToWrite.asByteBuffer());
    outputStream.close();
    fc.close();
  }
	
  public static SdkBytes readFromFile(String path) throws IOException {
    InputStream in2 = new FileInputStream(path);
    return SdkBytes.fromInputStream(in2);
  }
}

Encrypting and Decrypting Using Data Keys

Recall you cannot export CMKs from AWS KMS. You are also limited to encrypting data of 4kb or less. Both these limitations limit what you can encrypt using a CMK. Also note that you must send the data to the AWS KMS to encrypt/decrypt the data. Although convenient, using a CMK as we did in the preceding section is not ideal. Instead we should use the CMK as a master key that generates, encrypts, and decrypts data keys. You then use data keysThe CMK is responsible only for encrypting/decrypting data keys.

Data keys are designed to be used within your external application that resides outside KMS. Data keys can encrypt/decrypt data of any size and are stored in your own application.

Envelope Encryption is how AWS KMS protects the generated data key. The KMS creates a data key, encrypts the data key, and returns the encrypted data key version and the plain-text unencrypted version of the data key. The plain-text version of the key is what your application uses to encrypt and decrypt data. The encrypted version of the key is what your application saves to use later. You should always ensure the plain-text data key is deleted and removed from memory soon after use so your data’s security is not compromised. When your application needs to use the data key again, request that the AWS CMK decrypts the data key and then use that decrypted key locally.

In the example below we use the CMK above to generate a data key and use that data key to encrypt data. The steps to encrypt in this tutorial are as follows.

  1. Request that the CMK in KMS generates a data key.
  2. Load unencrypted data from file.
  3. Use plain-text version of returned data key to encrypt data.
  4. Store encrypted data to a file.
  5. Store encrypted version of key to a file.
Using a data key to encrypt data

After encrypting the data, we then decrypt the data using the data key. However, before we can decrypt the data using the data key we must first use the CMK to decrypt the data key. The process our application uses is as follows.

  1. Load encrypted data key from file.
  2. Request AWS KMS used to originally encrypt the data key to decrypt the data key.
  3. Load encrypted object from file.
  4. Use decrypted data key to decrypt object.
  5. Print decrypted data to console.
Using a data key to decrypt data

Although we use the KMS to work with the CMK, we must work with the data key locally. We do so using the java crypto package.

  • Modify KMSExample so that its import list contains the following.

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.channels.FileChannel;
import java.nio.file.Paths;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;

import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.core.SdkBytes;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.kms.KmsClient;
import software.amazon.awssdk.services.kms.model.DataKeySpec;
import software.amazon.awssdk.services.kms.model.DecryptRequest;
import software.amazon.awssdk.services.kms.model.DecryptResponse;
import software.amazon.awssdk.services.kms.model.EncryptRequest;
import software.amazon.awssdk.services.kms.model.EncryptResponse;
import software.amazon.awssdk.services.kms.model.GenerateDataKeyRequest;
import software.amazon.awssdk.services.kms.model.GenerateDataKeyResponse;

Encrypt Using Data Key

Let’s encrypt the data using a data key.

  • Create a new method named encryptUsingDataKey that takes the data to encrypt as an SDKBytes data object.
  • Create a GenerateDataKeyRequest using the KmsTutorialKey customer key.
  • Use the KmsClient to return the generated data key as a GenerateDataKeyResponse.
  • Use the Java crypto API to create a SecretKeySpec from the generated data key.
  • Create a Cipher and use it to encrypt the jsonString.
  • Write the encrypted data to a file.
  • Get the encrypted data key from the response and save it to a file.
public void encryptUsingDataKey(SdkBytes jsonString) {
  try {
    GenerateDataKeyRequest generateDataKeyRequest = GenerateDataKeyRequest
      .builder().keyId(keyArn)			 
      .keySpec(DataKeySpec.AES_128).build();
    GenerateDataKeyResponse generateDataKeyResponse = this.kmsClient
      .generateDataKey(generateDataKeyRequest);
    
    SecretKeySpec key = new 
      SecretKeySpec(generateDataKeyResponse.plaintext().asByteArray(), 
      "AES");
      Cipher cipher;
      cipher = Cipher.getInstance("AES");
      cipher.init(Cipher.ENCRYPT_MODE, key);
      byte[] encodedSecret = cipher.doFinal(jsonString.asByteArray());
      byte[] encryptedDataKey = key.getEncoded();
      String path = Paths.get(".").toAbsolutePath().normalize().toString() 
        + "/observation_datakey_encrypt.json";
			 
    KMSExample.writeToFile(SdkBytes.fromByteArray(encodedSecret),
     path);
    path = Paths.get(".").toAbsolutePath().normalize().toString() + 
      "/data_key_encrypt.json";
			 
   KMSExample.writeToFile(SdkBytes.fromByteArray(encryptedDataKey), path);
  } catch (Exception ex) {
      ex.printStackTrace();
  }
}

The GenerateDataKeyRequest wraps a request to generate a data key. You create a GenerateDataKeyRequest using a GenerateDataKeyRequest.Builder. The builder uses the CMK’s ARN and data keyspec to build the request. The KmsClient then passes the request to the KMS which uses the specified CMK to generate the data key. The data key is returned in a GenerateDataKeyResponse.

The AES specifies we wish our data key to use the Advanced Encryption Standard. We must use the same keyspec when using our data key that was used to generate the data key.

We take the plain-text (unencrypted) key version from the response and use it to build a SecretKeySpec. We then pass the SecretKeySpec to a Cipher, which encrypts the data using the Java Cryptography Extension framework. You can obtain more information by consulting this resource: Java Cryptography Architecture (JCA) Reference Guide

  • Modify main to call encryptUsingDataKey.
  • Remove the code that used the CMK to encrypt and decrypt the data.
kmsExample.encryptUsingDataKey(inputBytes);

The main method should appear as follows after adding the call to encrypt the data using a data key.

  public static void main(String[] args) {
    try 
    {
      KMSExample kmsExample = new KMSExample();
      String path = Paths.get(".").toAbsolutePath().normalize()
        .toString() + "/observation.json";
      kmsExample.encryptUsingDataKey(inputBytes);
    }
    catch (Exception e) {
      e.printStackTrace();
    }
  }
  • Build and run the program and there should be two files: data_key_encrypt.txt and observation_datakey_encrypt.json, the encrypted data key and the encrypted data respectively.
Encrypted data key stored locally on disk
Encrypted data stored locally on disk

Decrypting Using Data Key

Let’s decrypt the encrypted data key and use it to decrypt the encrypted data. We first decrypt the local data key by passing it to KMS which uses the CMK to decrypt the key. We then use the returned decrypted key to decrypt the data locally.

  • Create a new method named decryptUsingDataKey.
  • Read the encrypted data key from the file.
  • Create a new DecryptRequest from the data key.
  • Decrypt the data key by passing the DecryptRequest to the kmsClient.
  • Create a SecretKeySpec from the decrypted data key.
  • Read the encrypted data from a file.
  • Create a Cipher and use it and the SecretKeySpec to decrypt the data.

public void decryptUsingDataKey() {
  try {
    String path = Paths.get(".").toAbsolutePath().normalize().toString() + 
      "/resource/data_key_encrypt.txt";
    SdkBytes sdkBytes = KMSExample.readFromFile(path);
    DecryptRequest decryptRequest = DecryptRequest.builder()
      .ciphertextBlob(sdkBytes).build();
    DecryptResponse decryptResponse = this.kmsClient
      .decrypt(decryptRequest);
			
    SecretKeySpec secretKeySpec = new SecretKeySpec(decryptResponse
      .plaintext().asByteArray(), "AES");
    path = Paths.get(".").toAbsolutePath().normalize().toString() + 
      "/observation_datakey_encrypt.json";
    sdkBytes = KMSExample.readFromFile(path);
    Cipher cipher = Cipher.getInstance("AES");
    cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);
    System.out.println(SdkBytes.fromByteArray(cipher.doFinal(sdkBytes
      .asByteArray())).asUtf8String());	
  } catch(Exception ex) {
      ex.printStackTrace();
    }
}
  • Modify main to call the decryptUsingDataKey method.
kmsExample.decryptUsingDataKey();

The main method should appear as follows.

  public static void main(String[] args) {
    try 
    {
      KMSExample kmsExample = new KMSExample();
      String path = Paths.get(".").toAbsolutePath().normalize()
        .toString() + "/resource/observation.json";
      KMSExample.writeToFile(outputBytes, path);
      SdkBytes output2Bytes = kmsExample.deCrypt(KMSExample
        .readFromFile(path));
      System.out.println(output2Bytes.asUtf8String());

      kmsExample.encryptUsingDataKey(inputBytes);
      kmsExample.decryptUsingDataKey();
    }
    catch (Exception e) {
      e.printStackTrace();
    }
  }
  • Build and run the application. You should see the JSON record printed to the console.
{
      "stationid": 221,
      "date": "1992-03-12",
      "time": "091312",
      "message":"This is a secret message. Please encrypt it when storing on disk."
}

Conclusion

Amazon’s KMS is a convenient and powerful service to manage your organization’s keys. It is integrated with most AWS Services. You can also use it directly in your application, as demonstrated in this tutorial. The most common use pattern is to create a CMK which must reside in KMS as your master key. That CMK is then used to create local data keys. The local data keys encrypt/decrypt the data. Only the encrypted version of the data key should ever be persisted locally. Instead, whenever the local data key is needed it is passed to KMS so that the associated CMK can decrypt the data key.

You can also administer KMS using the Java API; however, in this tutorial we restricted ourselves to decrypting and encrypting data.

More Resources

Here are two introductory videos on KMS. Neither are programming specific, but they both provide a greater understanding of KMS.

Git Project

The github project is available here. https://github.com/jamesabrannan/kmstutorial