Bossy Lobster

A blog by Danny Hermes; musing on tech, mathematics, etc.

Edit on GitHub

Decrypting Vault Ciphertext with a Context

In a previous post, I described a use case for customer provided keys with Vault. One of the implications of this was the need for decryption after a bulk data export. In that post, I gave a concrete example of decrypting Vault ciphertext directly with a customer provided key. However, it's also very common to encrypt using a derived key, created with a user-supplied context. In this post, we'll give a concrete example of generating a derived key and then decrypting Vault ciphertext with it.

Initialize

First, start a test Vault instance with transit encryption enabled:

$ docker run \
>   --rm \
>   --detach \
>   --name vault-dev-server \
>   --publish 8200:8200 \
>   --cap-add IPC_LOCK \
>   --env VAULT_DEV_ROOT_TOKEN_ID=root \
>   vault:1.8.3
$
$ export VAULT_TOKEN=root VAULT_ADDR=http://localhost:8200
$ vault secrets enable transit
Success! Enabled the transit secrets engine at: transit/

Then simulate a customer provided key and require that a user-supplied context must always be used by setting derived=true:

$ vault write transit/keys/acme-co-provided \
>   type=aes256-gcm96 \
>   allow_plaintext_backup=true \
>   exportable=true \
>   derived=true
Success! Data written to: transit/keys/acme-co-provided

Encrypt Data

If we try to encrypt without a context, it will fail since key derivation is required:

$ echo -n 'FOO' | base64
Rk9P
$ vault write transit/encrypt/acme-co-provided plaintext=Rk9P
Error writing data to transit/encrypt/acme-co-provided: Error making API request.

URL: PUT http://localhost:8200/v1/transit/encrypt/acme-co-provided
Code: 400. Errors:

* missing 'context' for key derivation; the key was created using a derived
  key, which means additional, per-request information must be included in order
  to perform operations with the key

Instead, we encrypt the data with the context {"bar":"baz"}:

$ echo -n '{"bar":"baz"}' | base64
eyJiYXIiOiJiYXoifQ==
$ vault write transit/encrypt/acme-co-provided plaintext=Rk9P context=eyJiYXIiOiJiYXoifQ==
Key            Value
---            -----
ciphertext     vault:v1:1pEqzFnkQEa5RA35ynhOd0Ye907S9PvWIq5dRPDP3Q==
key_version    1

Read Key Inputs

First, take note of the key derivation function used and the key type:

$ vault read transit/keys/acme-co-provided
Key                       Value
---                       -----
...
kdf                       hkdf_sha256
...
type                      aes256-gcm96

Then, export the key via the backup API:

$ vault read transit/backup/acme-co-provided
Key       Value
---       -----
backup    eyJwb2xpY3kiOnsibmFtZS...
$
$ vault read --field=backup transit/backup/acme-co-provided | base64 --decode | jq
{
  "policy": {
    "name": "acme-co-provided",
    "keys": {
      "1": {
        "key": "rph2pwTQCx+TD/lk+7o9igzQw5A7FU3+S+Z24Cf9Duk=",
        ...
      }
    },
    ...
  },
  ...
}

Decrypt Ciphertext

With the key, algorithms and ciphertext in hand, we are ready to decrypt. First, create a derived key using the context:

>>> import cryptography.hazmat.primitives.hashes
>>> algorithm = cryptography.hazmat.primitives.hashes.SHA256()
>>>
>>> import cryptography.hazmat.backends
>>> backend = cryptography.hazmat.backends.default_backend()
>>>
>>> import base64
>>> context = base64.b64decode(b"eyJiYXIiOiJiYXoifQ==")
>>> context
b'{"bar":"baz"}'
>>>
>>> import cryptography.hazmat.primitives.kdf.hkdf
>>> hkdf = cryptography.hazmat.primitives.kdf.hkdf.HKDF(
...     algorithm=algorithm, length=32, info=context, backend=backend, salt=None
... )
>>>
>>> key = base64.b64decode("rph2pwTQCx+TD/lk+7o9igzQw5A7FU3+S+Z24Cf9Duk=")
>>> derived_key = hkdf.derive(key)
>>> len(derived_key)
32
>>> derived_key
b'uU\x18\xdf...'

Then, decrypt the ciphertext with the derived key:

>>> encoded = "1pEqzFnkQEa5RA35ynhOd0Ye907S9PvWIq5dRPDP3Q=="
>>> iv_and_ciphertext = base64.b64decode(encoded)
>>> iv_bytes = iv_and_ciphertext[:12]
>>> ciphertext_bytes = iv_and_ciphertext[12:]
>>>
>>> import cryptography.hazmat.primitives.ciphers.aead
>>> aead_cipher = cryptography.hazmat.primitives.ciphers.aead.AESGCM(derived_key)
>>> aead_cipher.decrypt(iv_bytes, ciphertext_bytes, None)
b'FOO'

Comments