Encrypting and decrypting data keys with AWS KMS

It sometimes happens that applications need to use sensitive, user-specific data to move on to the next step. It's a bad practice to store sensitive data as is in the database. AWS KMS comes with two methods which can help encrypting and decrypting the sensitive pieces of information.

AWS Key Management System is a fully managed encryption service.

1. About KMS

KMS creates and securily stores keys with which we can encrypt and decrypt data up to 4 kB.

AWS creates some default Customer Master Keys (CMKs) for the services like S3 and EBS, when we decide to encrypt data using the services. This part is fully managed without any interaction from the account owner. These keys have the format of aws/SERVICE_NAME, like aws/s3, which is responsible for server-side encryption in S3 (It’s not the CMK though that encrypts the multiple GB data on S3. CMK only encrypts the data key which then encrypts the large amount of data, see the size constraint of 4 kB).

KMS also provides accounts the option to create customer managed CMKs. These keys can be used to encrypt user-specific data like private keys, secrets and passwords. This way, they can be safely stored in a database, as they are now secure.

One use case is when each user of an application has a private key, which, by nature, is supposed to be private, and no one else should have access to it. The private key can be used to ensure that the person is really the right person in asymmetric cryptography.

But what if they still need to be stored because a part of the application uses them?

With KMS CMKs, these private keys can be encrypted and safely stored in the database next to the user’s other pieces of data.

2. Pre-requisites

To create CMKs and use them in application, one needs to have a free AWS account, user credentials for programmatic access to AWS services and the AWS CLI installed.

Warning: Following the tutorial below comes with a small cost (about 20-30 cents).

3. Example

The following very simple example illustrates how sensitive user information can be encrypted and decrypted by KMS using the SDK for Node.js

3.1. Create a CMK

Creating a CMK is easy. We can use the CLI with the following command:

aws kms create-key --description Test key --region us-west-2

The region can be any valid AWS region. The response will contain the ID of the key, which will be needed in the application.

We can check if the key indeed has been created by typing

aws kms list-keys --region us-west-2

The response will list both AWS default keys (used to encrypt data for other services) and the CMK that was created in the last step:

{
  "Keys": [
    # other keys here
    {
      "KeyId": "cb2c8f93-2562-489e-b612-abac83557c1d",
      "KeyArn": "arn:aws:kms:us-west-2:<ACCOUNT ID>:key/cb2c8f93-2562-489e-b612-abac83557c1d"
    }
  ]
}

From this response, it’s hard to decide which key is the one that needs to be used in the application, especially if the account owner runs multiple applications across multiple services.

The following command helps to get more info about the key:

aws kms describe-key --key-id cb2c8f93-2562-489e-b612-abac83557c1d

We’ll get back something similar:

{
  "KeyMetadata": {
    "AWSAccountId": "<ACCOUNT ID>",
    "KeyId": "cb2c8f93-2562-489e-b612-abac83557c1d",
    "Arn": "arn:aws:kms:us-west-2:<ACCOUNT ID>:key/cb2c8f93-2562-489e-b612-abac83557c1d",
    "CreationDate": 1560769500.027,
    "Enabled": true,
    "Description": "Test key",
    "KeyUsage": "ENCRYPT_DECRYPT",
    "KeyState": "Enabled",
    "Origin": "AWS_KMS",
    "KeyManager": "CUSTOMER"
  }
}

The KeyManager property of the response object says that it has been created by CUSTOMER, i.e. this is the key we created above. For AWS default keys, this value will be AWS.

Also, the Description property reflects the description what we submitted with the create-key command.

3.2. Set up the application

Create the project folder, initiate it with npm init -y, and install aws-sdk by typing npm install aws-ask.

3.3. Encrypt the private key

Let’s create a file called encrypt.js, and then import the SDK and the crypto module. The code can look something like this:

const AWS = require('aws-sdk')
const crypto = require('crypto')

const privateKey = crypto.randomBytes(64)

const kms = new AWS.KMS({
  region: 'us-west-2'
})

const paramsForEncrypt = {
  KeyId: 'cb2c8f93-2562-489e-b612-abac83557c1d',
  Plaintext: privateKey.toString('hex')
}

function encryptPrivateKey(params) {
  return kms.encrypt(params).promise()
}

encryptPrivateKey(paramsForEncrypt)
  .then(({ CiphertextBlob }) => {
    // store it in database with other user data
    storeEncryptedPrivateKeyInDB(CiphertextBlob) // method is not defined
  }).catch((e) => {
    console.log(e)
  })

We can simulate the private secret key with a 64-byte long series of characters created by the randomBytes method of the crypto module.

Then, the KMS class is initiated, and the instance is saved to a variable named kms.

The paramsForEncrypt object will be provided as an argument to the encrypt method of kms.

KeyId is the id of the key we want to use for encryption. This is the key which was created above using the CLI. The second property is Plaintext, which is the generated random sequence acting now as the private key. This is what we want to encrypt with the help of KMS.

The encryptPrivateKey function does the encryption by returning the encrypt method. By default, encrypt accepts a callback as a second argument, but this can be be removed from the method, which can be converted so that it returns a promise by chaining the promise method to it.

As such, the encrypted private key can be seen inside then, and it’s called CiphertextBlob in the response object. We can now safely save it to the database.

3.4. Decrypt the private key

Decrypting the already encrypted private key follows a very similar logic.

The code can look like this:

const AWS = require('aws-sdk')

const { getEncryptedPrivateKey } = require('./db')

const paramsForDecrypt = {
  // fetch encrypted private key from database
  CiphertextBlob: getEncryptedPrivateKeyFromDB()
}

function decryptPrivateKey(params) {
  return kms.decrypt(params).promise()
}

decryptPrivateKey(paramsForDecrypt)
  .then((data) => {
    console.log(data)
  }).catch((e) => {
    console.log(e)
  })

The key difference (haha) is that we don’t need to specify the key, because KMS will recognize and returns it in the response data:

# data
{
  KeyId: 'arn:aws:kms:us-west-2:<ACCOUNT ID>:key/cb2c8f93-2562-489e-b612-abac83557c1d',
  Plaintext:
   <Buffer 38 35 37 39 36 37 37 39 37 37 35 36 63 30 33 63 65 37 34 65 66 37 31 37 37 34 65 63 32 34 34 31 63 37 38 35 31 62 36 35 31 63 32 30 66 30 37 36 63 36 ... >
}

The decrypted key comes back as a Buffer, so we’ll need to convert it to string to get back the originally encrypted, then now decrypted private key.

4. Clean-up

CMKs cost $1 a month each, so if they are not needed, it’s a good idea to remove them to avoid getting charged.

CMKs cannot get deleted straight away, instead we can schedule their deletion, and when it’s done, a waiting period between 7 and 30 days applies (the default is 30 days).

This is a preventive step from AWS, because data that had been encrypted with that CMK cannot be decrypted after the key is deleted.

We can use the following CLI command to schedule deletion:

aws kms schedule-key-deletion --key-id cb2c8f93-2562-489e-b612-abac83557c1d --pending-window-in-days 7

In this case, the CMK which was created at the beginning of this post is scheduled for deletion in 7 days, after which AWS deletes the key. The response confirms the date of deletion:

{
  "KeyId": "arn:aws:kms:us-west-2:<ACCOUNT ID>:key/cb2c8f93-2562-489e-b612-abac83557c1d",
  "DeletionDate": 1561507200.0
}

When the key has been scheduled for deletion, it cannot be used for encryption.

5. Conclusion

One use case of KMS Customer Master Keys is to encrypt sensitive data of small size (up to 4096 bytes). Typically, these data can be private keys, secrets or passwords, which need to be saved to a database, but it’s not a good practice to do so in their raw format.

The encrypt and decrypt methods can be used for encryption and decryption.

Thanks for reading, and see you next time.