Andreas Garnæs

Envelope Encryption with Amazon KMS

Amazon Key Management Service is a service for creating and controlling encryption keys in a safe manner, using Hardware Security Modules under the hood. KMS also offers hassle-free yearly key rotation, and logs all key usage to CloudTrail by default.

Using KMS, there are generally two ways to encrypt data: use the Encrypt endpoint or use envelope encryption. This posts tries to provide an overview of the two.

The code snippet beneath will be the scaffold for our example code, which reads the sensitive data from stdin, encrypts it, prints the ciphertext and then decrypts it again:

 1 import (
 2     "fmt"
 3     "io/ioutil"
 4     "os"
 5     "time"
 6 
 7     "github.com/awslabs/aws-sdk-go/aws"
 8     "github.com/awslabs/aws-sdk-go/gen/kms"
 9 )
10 
11 const (
12     region = "eu-west-1"         // <-- change if needed
13     keyId = "INSERT-KEY-ID-HERE" // <-- insert key id here
14 )
15 
16 func main() {
17     plaintext, err := ioutil.ReadAll(os.Stdin)
18     if err != nil {
19         panic(err)
20     }
21 
22     // Create client
23     creds, err := aws.ProfileCreds("", "", 10*time.Minute)
24     if err != nil {
25         panic(err)
26     }
27     kmsClient := kms.New(creds, region, nil)
28 
29     // Encrypt plaintext
30     encrypted, err := Encrypt(kmsClient, plaintext) // <-- to be implemented
31     if err != nil {
32         panic(err)
33     }
34     fmt.Println("Encrypted: ", encrypted)
35 
36     // Decrypt ciphertext
37     decrypted, err := Decrypt(kmsClient, encrypted) // <-- to be implemented
38     if err != nil {
39         panic(err)
40     }
41     fmt.Println("Decrypted: ", decrypted)
42 }

The code assumes you have a Amazon credentials file in ~/.aws/credentials, that you're in region eu-west-1 (line 12), and that you have a KMS key id (line 13). If you don't have a KMS key, here's how to create one.

Encrypt Endpoint

Using the Encrypt endpoint and Decrypt endpoint, it's easy to get started with protecting your data, and your application is never exposed to any keys or crypto. The caveats are latency, cost and an Amazon-imposed limit of 4KB on the size of the plaintext. This is enough for encrypting an RSA key, a database password or credit card information, though. Here's an implementation of Encrypt and Decrypt from the scaffold above, which uses these endpoints:

 1 func Encrypt(kmsClient *kms.KMS, plaintext []byte) ([]byte, error)
 2     encryptRsp, err := kmsClient.Encrypt(&kms.EncryptRequest{
 3         Plaintext: plaintext,
 4         KeyID:     aws.String(keyId),
 5     })
 6     if err != nil {
 7         panic(err)
 8     }
 9     return encryptRsp.CiphertextBlob, nil
10 }
11 
12 func Decrypt(kmsClient *kms.KMS, ciphertext []byte) ([]byte, error) {
13     decryptRsp, err := kms.Decrypt(&kms.DecryptRequest{
14         CiphertextBlob: ciphertext,
15     })
16     if err != nil {
17         panic(err)
18     }
19     return decryptRsp.Plaintext, nil
20 }

As you can tell it's quite simple.

Envelope Encryption

KMS customer master keys are never exposed to the user, so they cannot be used for encrypting data locally. Instead, KMS offers the ability to generate new keys, which are exposed both in plaintext and in encrypted form. Data keys can then be used for encrypting locally, thus sidestepping the 4kB limit and the roundtrip to KMS. This is what is described as "envelope encryption" in the KMS documentation . In more detail, the technique is as follows:

  1. Generate a data key with the GenerateDataKey endpoint to obtain a key both as plaintext and ciphertext (encrypted).
  2. Use the plaintext key to encrypt the payload.
  3. Transmit the encrypted key along with the encrypted payload.
  4. To decrypt the data, use KMS to decrypt the encrypted key and then decrypt the payload with the plaintext key.

To actually encrypt the message (step 2), the application will have to use a separate encryption scheme. The following example will be using secretbox from NaCl. To encrypt or decrypt a message with secretbox, you need both a key (secret) and a nonce (random data, not secret). The encrypted message will then be bundled up together with the key as encrypted with KMS and the nonce.

Here's the code to implement the above scheme:

 1 import (
 2     "bytes"
 3     "encoding/gob"
 4 
 5     "golang.org/x/crypto/nacl/secretbox"
 6 )
 7 
 8 const (
 9     keyLength   = 32
10     nonceLength = 24
11 )
12 
13 type payload struct {
14     Key     []byte
15     Nonce   [nonceLength]byte
16     Message []byte
17 }
18 
19 func Encrypt(kmsClient *kms.KMS, plaintext []byte) ([]byte, error) {
20     // Generate data key
21     rsp, err := kmsClient.GenerateDataKey(&kms.GenerateDataKeyRequest{
22         KeyID:         aws.String(keyId),
23         NumberOfBytes: aws.Integer(keyLength),
24     })
25     if err != nil {
26         return nil, err
27     }
28 
29     // Initialize payload
30     p := &payload{
31         Key:   rsp.CiphertextBlob,
32         Nonce: [nonceLength]byte{},
33     }
34 
35     // Set nonce
36     if _, err = rand.Read(&p.Nonce[:]); err != nil {
37         return nil, err
38     }
39 
40     // Create key
41     key := &[keyLength]byte{}
42     copy(key[:], rsp.Plaintext)
43 
44     // Encrypt message
45     p.Message = secretbox.Seal(p.Message, plaintext, &p.Nonce, key)
46 
47      // Encode to bytes
48     buf := &bytes.Buffer{}
49     if err := gob.NewEncoder(buf).Encode(p); err != nil {
50         return nil, err
51     }
52 
53     return buf.Bytes(), nil
54 }
55 
56 func Decrypt(kmsClient *kms.KMS, ciphertext []byte) ([]byte, error) {
57     // Decode bytes to a payload
58     var p payload
59     gob.NewDecoder(bytes.NewReader(ciphertext)).Decode(&p)
60 
61     // Decrypt key
62     decryptRsp, err := kmsClient.Decrypt(&kms.DecryptRequest{
63         CiphertextBlob: p.Key,
64     })
65     if err != nil {
66         return nil, err
67     }
68 
69     // Create key
70     key := &[keyLength]byte{}
71     copy(key[:], decryptRsp.Plaintext)
72 
73     // Decrypt message
74     var plaintext []byte
75     plaintext, ok := secretbox.Open(plaintext, p.Message, &p.Nonce, key)
76     if !ok {
77         return nil, fmt.Errorf("Failed to open secretbox")
78     }
79     return plaintext, nil
80 }

Note that this code generates a new data key for each encrypted value, but the data key could just as well be generated once and reused. For simplicity, I've used gob to encode a payload of key, nonce and message to bytes.

If you want to give it a try, you can download the sample code. Run it like this after you've changed the key id:

> echo "My very secret data" | go run envelope_encryption.go

As evident from the two code samples above, envelope encryption is more complex than using the encrypt/decrypt endpoints, but depending on your use-case, it might be a requirement for speed or size of data.

comments powered by Disqus