AWS Key Management Service Envelope Encryption in PHP

This is a PHP example of what AWS calls envelope encryption. Really this is just a way to use a key hierarchy rooted at a key management service (KMS) key.

We’ll use PHP 7.2’s libsodium support (via paragonie/sodium_compat).

The idea is that you have a customer master key that lives in KMS – this never leaves the service. Using that master key you can make encrypt or decrypt requests or, as in this example, generate data keys.

Encrypting Data

We’ll generate a data key and use it to encrypt our sensitive information in application. In a real app, we’d then store the ciphertext we generate locally along with the ciphertext of the our data key that comes back from KMS.

require __DIR__.'/vendor/autoload.php';
use Aws\Kms\KmsClient;

$kms = KmsClient::factory([
    'region' => 'us-east-1',
    'version' => 'latest',
]);

// generate a data key.
$dataKey = $kms->generateDataKey([
    'KeyId' => getenv('KMS_KEY_ID'),
    'NumberOfBytes' => SODIUM_CRYPTO_SECRETBOX_KEYBYTES,
]);

// sodium requries a nonce, this isn't sensitive, but it we'll need
// the same value for both encryption and descryption.
$nonce = sodium_randombytes_buf(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);

// encrypt! then remove the $dataKey, might be better to
// use sodium_memzero on the key plaintext here.
$ciphertext = sodium_crypto_secretbox(
    'Hello, World',
    $nonce,
    $dataKey['Plaintext']
);
$ciphertextKey = $dataKey['CiphertextBlob'];
unset($dataKey);

// store $ciphertext, $nonce, and $ciphertextKey somehow

Why Put the Key ID in the Environment?

KMS makes it seem more like encryption keys are a backing service. Because of that, I think it makes a lot of sense to store that configuration in the same way one might store other back service information.

Decrypting Data

On the way back out, you request that KMS decrypts the data key cipher text, then use that to decrypt the data in application.

// $kms is setup as in the example above

$dataKey = $kms->decrypt([
    'CiphertextBlob' => $ciphertextKey,
]);

$plaintext = sodium_crypto_secretbox_open(
    $ciphertext,
    $nonce,
    $dataKey['Plaintext']
);
unset($dataKey);

var_dump($plaintext); // string(12) "hello, world"

Downsides

This is a bit slow. If you’re making KMS calls in a hot code path, it’s probably not going to perform that great. You might have to consider a different key hierarchy.

A Word about Encryption Context

One of the coolest things KMS provides is encryption context. When provided (to generateDataKey or decrypt) this is used as part of the encryption algorithm in KMS itself as well as provides some additional audit log information. All calls to the KMS APIs will send this encryption context into CloudTrail so it can be inspected later.

Encryption context must be the same for an encrypt and decrypt call. Meaning if you pass context to generateDataKey the same context must be passed to decrypt that data keys ciphertext.

Other Resources