For legacy reasons one of my systems doesn't have the option of using an AEAD mode, we are restricted to AES in plain CBC or CTR mode plus a MAC.
A typical task is to transfer data from one node to another while guaranteeing integrity and confidentiality. I find myself repeatedly specifying the following composition:
- CSPRNG to generate a bootstrap secret
- KDF to derive keys for encryption and MAC - I use HKDF
- CSPRNG again the get an iv
- CTR mode to encrypt the data
- MAC over bootstrap secret, iv, cipherspec and ciphertext - I use HMAC
So I am doing Encrypt-then-MAC and I am authenticating all the inputs to the ciphertext calculation. But I have kind of blithely assumed this composition is secure.
I've never actually seen this full composition including the KDF described as a reusable primitive.
TLS does something very similar, but it's not quite the same (e.g. it uses HKDF differently).
IES schemes like ECIES and DLIES look conceptually similar, but differ in the details, especially in the way the inputs to the KDF are derived.
So my question is: is this problem insufficiently general to warrant a cookbook solution? Or maybe something already exists that I've overlooked? Otherwise how can I gain confidence in the solution? (When it comes to crypto I'm always cautious).
In case the details are useful, the flow is:
The sending node performs the following:
- Obtain 256 secret bits
seed
from a CSPRNG
- Encrypt
seed
for the other node using its public key as encrypted_seed
- Split
seed
into 128 bits salt
and 128 bits key_material
- Derive 384 secret bits by calling
HKDF-HMAC-SHA-256(length=384b, ikm=key_material, salt=salt, info=<source node id || dest node id>)
- Split up into 128 bit
encryption_key
, 256 bit HMAC_key
For each message to be sent:
- Obtain 128 bits from a CSPRNG as
encryption_iv
- Encrypt the plaintext using
AES-128-CTR(iv=encryption_iv, key=encryption_key)
- Calculate tag as
HMAC-SHA-256(key=HMAC_key, data=encrypted_seed || encryption_iv || cipherspec=AES-128-CTR || ciphertext)
- Send to the other node:
encrypted_seed || encryption_iv || cipherspec || ciphertext || tag
The receiving node performs the following:
- Parse received message into its components
encrypted_seed
etc.
- Decrypt
encrypted_seed
using the receiving node's private key, obtaining seed
- Split
seed
into 128 bits salt
and 128 bits key_material
- Derive 384 secret bits by calling
HKDF-HMAC-SHA-256(length=384b, ikm=key_material, salt=salt, info=<source node id || dest node id>)
- Split up into 128 bit
encryption_key
, 256 bit HMAC_key
- Calculate tag as
HMAC-SHA-256(key=HMAC_key, data=<message as received from sending node without tag>)
- Assign
tag_valid := true
if tag matches the one on the received message, false
otherwise
- Assign
k := encryption_key
if tag_valid
, otherwise assign k := <some random constant>
- Decrypt the ciphertext as
[cipherspec](iv=encryption_iv, key=k)
- Output a tuple
(tag_valid, plaintext)
- the caller is responsible to check tag_valid
before using plaintext
So what could go wrong? Well, for one the seed
value is used before the MAC tag is checked. I could sign it using the sending node's private key, but that doesn't actually bind seed
to the MAC tag. Also this is starting to get messy, hence my uneasiness.