Encryption Context (AAD) with AWS KMS
Introduction - AADAWS KMS API supports authenticated encryption using additional authentication data (AAD). The concept of AAD is explained in AWS’ documentation. However, as this is not trivial, I have decided to put that into a live example in this blog post and a simple web application example (available at this GitHub repository).
Problem statementLet’s assume a team developed a web interface (RESTful API) providing users with a secrets store; the service has two simple APIs:
- The first API call (POST) is used to upload a secret (providing a secret name and value)
- Another API call (GET) is used to retrieve secrets uploaded using the API above
In memory the secrets are stored encrypted (standard envelope encryption). For service authentication the team had setup BASIC AUTH on the REST API access. So far pretty simple – not brilliant but a straight forward example which can demonstrate the usage if AAD.
Pretty soon the team discovers that some users had found out that they could fetch other users’ secrets just by tampering the URL. The problem can be described using a few simple steps as:
- Alice stores a secret named ‘secret-a’ (HTTP POST to http://server/secrets/secret-a)
- The server encrypts the secret and stores it in memory
- After a while Bob finds out he could access the URL to fetch Alice's secret (GET to the same URL as above)
- In that case the server decrypts the secret and returns it
NB: In real life we would expect a service like that to tag a secret with the owner and handle that but I am taking the simplest path in here to the benefit of AAD demonstration.
Encryption Context as AADThe idea behind Additional Authenticated Data (AAD) is that encrypted data belongs to a context and that context should be validated when we attempt to decrypt the ciphertext. If we provide AAD to AWS KMS encryption API call it will tag the ciphertext with some context. When we attempt to decrypt that ciphertext an identical context must be provided, otherwise the decryption will fail.
In the example above the simplest context we could use is the user owning the secret – only Alice should be able to access her secrets, Bob belongs to a different context. AWS implementation refers to that context as EncryptionContext which is, essentially, a Map<String, String>. The two code samples below illustrate the usage of EncryptionContext in AWS, starting with the encryption API.
// We start by generating a data key and binding it to a context GenerateDataKeyRequest dataKeyRequest = new GenerateDataKeyRequest() .withKeyId(cmkAlias).withKeySpec("AES_128") .withEncryptionContext( Collections.singletonMap("user", principal.getName())); GenerateDataKeyResult dataKeyResult = awskms.generateDataKey(dataKeyRequest); // The data key is just raw material - build a JCE key for Java Key key = buildJCEKey(dataKeyResult.getPlaintext().asReadOnlyBuffer()); dataKeyResult.getPlaintext().clear(); // Clear it ASAP!! // Encryption: standard Java API String encrypted = encrypt(value, key); // Envelope: // - The data key in the encrypted form (needs the master key to open) // - The encrypted payload Envelop envelope = new Envelope(dataKeyResult.getCiphertextBlob(), encrypted);
The code above creates a unique data key for my envelope encryption (see here about envelope encryption) and tags that key’s encryption with AAD. As I have a mapping of one to one between data keys and the encrypted data I, effectively, tag the encrypted data with its context. Opening the encryption will involve code similar to the following:
// Get the encrypted data key and open using the CMK DecryptRequest decryptRequest = new DecryptRequest().withCiphertextBlob(envelope.key) .withEncryptionContext( Collections.singletonMap("user", principal.getName())); DecryptResult decryptResult = awskms.decrypt(decryptRequest); // Build a JCE Key out of it - for Java to use Key key = buildJCEKey(decryptResult.getPlaintext().asReadOnlyBuffer()); decryptResult.getPlaintext().clear(); // Decrypt the actual secret String cleartext = decrypt(envelope.payload, key);
The most important part of that example is the usage of ‘withEncryptionContext’ on both sides of the encryption – as I calculate the context (in this case the authenticated user) at runtime the encrypted form of the data key is tagged with that username (‘user=Alice’ when Alice stores a secret). If Bob changes the URL to point to Alice's secrets he will still fail: as long as he logged in as Bob the system will calculate ‘user=Bob’ as the context and fail to decrypt the data key - Alice's secret is now secured.
What Should be In the Context?There are a few aspects to be aware of when deciding on the context:
- Reproducible – obviously I would like to be able to reproduce the context otherwise I will not be able to decrypt the cipher text, so any context containing random generated values (e.g. UUID, timestamps) is not valid unless I store it somewhere
- Context can be stored (for reproducibility) but it is better to store the minimal required to calculate the full context at runtime. By that we reduce the surface attack of moving around the ciphertext and the context to trick the system
- Don’t include any sensitive data in the context – at least in AWS the context is logged to CloudTrail
Giving it a tryThe code above available on both Github (here) and as a Docker image on Docker Hub (here). Before running the example do remember that as this using AWS KMS it does imply that (1) it will attempt to create a CMK on the KMS if it cannot find one with the ‘aad-example’ alias (this alias can be overridden using standard Spring boot facilities), (2) CMKs cost money – so running this will be charged by AWS and (3) it needs to connect with AWS using access key and secret to be provided to the SDK.
To read more about build and executing the example check the README in GitHub.
Once the service is running try the following commands to see it in action:
[eyal@localhost]$ curl -X POST 'Content-type: text/plain' -vv -d 'my-secret-value' --user alice:alice http://localhost:8080/secrets/my-secret [eyal@localhost]$ curl -X GET -H 'Content-type: text/plain' -vv --user alice:alice http://localhost:8080/secrets/my-secret [eyal@localhost]$ #The next should fail since Bob is trying to fetch Alice’s secret [eyal@localhost]$ curl -X GET -H 'Content-type: text/plain' -vv --user bob:bob http://localhost:8080/secrets/my-secret