Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save Spaider/8fd0c97fd4785011032bc8144d00b8cc to your computer and use it in GitHub Desktop.
Save Spaider/8fd0c97fd4785011032bc8144d00b8cc to your computer and use it in GitHub Desktop.
Envelope Encryption using AWS KMS, Python Boto, and PyCrypto.

If you use Amazon AWS for nearly anything, then you are probably familiar with KMS, the Amazon Key Management Service.

KMS is a service which allows API-level access to cryptographic primitives without the expense and complexity of a full-fledged HSM or CloudHSM implementation. There are trade-offs in that the key material does reside on servers rather than tamper-proof devices, but these risks should be acceptable to a wide range of customers based on the care Amazon has put into the product. You should perform your own diligence on whether KMS is appropriate for your environment. If the security profile is not adequate, you should consider a stronger product such as CloudHSM or managing your own HSM solutions.

The goal here is to provide some introductory code on how to perform envelope encrypt a message using the AWS KMS API.

KMS allows you to encrypt messages of up to 4kb in size directly using the encrypt()/decrypt() API. To exceed these limitations, you must use a technique called "envelope encryption".

Read more about that here: http://docs.aws.amazon.com/kms/latest/developerguide/workflow.html

The steps are:

  1. Generate a new Customer Master Key using the Boto API or the AWS Console. Note that CMKs are region-specific, so you will need to generate keys per region in a multi-region configuration.
  2. Generate a Data Encryption Key via the generate_data_key() API. This API will return the Plaintext key, so take care with this field and clear it from memory when no longer needed. The CiphertextBlob is the Plaintext-key encrypted under the CMK. You will need to preserve this data for decryption purposes.
  3. Locally encrypt your data. In this example, we use PyCrypto's implementation of AES using their defaults (CFB mode, no IV), so be sure you understand this thoroughly before using any example code in your production environment.
  4. Store your locally encrypted data with the CiphertextBlob.
  5. When decryption is needed, pass the CiphertextBlob to the KMS decrypt() API which will return the Plaintext encryption key.
  6. Use PyCrypto's AES routines to create a new context and decrypt the encrypted ciphertext.

This script was forked from https://gist.github.com/pmp/60b28cb52914a6d3a4af and adapted to boto3 and Python 3

#!/usr/bin/env python
import base64

import boto3
from Crypto.Cipher import AES


PAD = lambda s: s + (32 - len(s) % 32) * ' '


def get_arn(aws_data):
    return 'arn:aws:kms:{region}:{account_number}:key/{key_id}'.format(**aws_data)


def encrypt_data(aws_data, plaintext_message):
    kms_client = boto3.client(
        'kms',
        region_name=aws_data['region'])

    data_key = kms_client.generate_data_key(
        KeyId=aws_data['key_id'],
        KeySpec='AES_128')

    cipher_text_blob = data_key.get('CiphertextBlob')
    plaintext_key = data_key.get('Plaintext')

    # Note, does not use IV or specify mode... for demo purposes only.
    cypher = AES.new(plaintext_key)
    encrypted_data = base64.b64encode(cypher.encrypt(PAD(plaintext_message)))

    # Need to preserve both of these data elements
    return encrypted_data, cipher_text_blob


def decrypt_data(aws_data, encrypted_data, cipher_text_blob):
    kms_client = boto3.client(
        'kms',
        region_name=aws_data['region'])

    decrypted_key = kms_client.decrypt(CiphertextBlob=cipher_text_blob).get('Plaintext')
    cypher = AES.new(decrypted_key)

    return cypher.decrypt(base64.b64decode(encrypted_data)).rstrip()


def main():
    # Add your account number / region / KMS Key ID here.
    aws_data = {
        'region': 'us-east-1',
        'account_number': '379419836112',
        'key_id': 'b9296038-f9e2-494d-b657-7202a671cb15',
    }

    # And your super secret message to envelope encrypt...
    plaintext = 'Hello, World!'

    # Store encrypted_data & cipher_text_blob in your persistent storage. You will need them both later.
    encrypted_data, cipher_text_blob = encrypt_data(aws_data, plaintext)
    print(encrypted_data)

    # # Later on when you need to decrypt, get these from your persistent storage.
    decrypted_data = decrypt_data(aws_data, encrypted_data, cipher_text_blob)
    print(decrypted_data)

if __name__ == '__main__':
    main()
@novelview9
Copy link

Awesome

@fuzzyami
Copy link

this fine example is now broken: AWS.new now requires that the mode and iv will be provided explicitly.

@tobiasgoecke
Copy link

@fuzzami did you update this example?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment