Applying a SHA 256 HMAC to an AES 256 cipher in PHP

In a previous post we have looked at encrypting data using AES in PHP. Today we look at how to ensure integrity of the message when encrypting. Doing AES one could simply send the following over the wire (using a randomized IV for every message):

Message = IV || AES-256-Encrypt(Plain-Text)

But this scheme does not allow you to see if there is a corruption in the message. Only if the plaintext is human language, you can see that it does not make sense. But then you have to rely on meta-knowledge about the structure of the plaintext. If you want to automatically detect tampering you need to add a cryptographic hash. A naive way of doing this would be to add a plain SHA-256 like this:

Message = IV || AES-256-Encrypt(Plain-Text || SHA-256(Plain-Text))

But if you have read a little bit about cryptography, you would probably know that you should be using a MAC (Message Authentication Code). MAC algorithms require two parties to share a secret key. MAC algorithms based on cryptographic hash functions (like SHA-256) are called HMAC and are defined in rfc2104. In PHP you can calculate a HMAC using the function “hash_hmac“. It would be convenient, but wrong, to use the same key for the AES and the HMAC and do:

Message = IV || AES-256-Encrypt(Plain-Text || SHA-256-HMAC(Plain-Text))

In the above scheme you take then plaintext and calculate a SHA-256-HMAC over it. You add the result to the end of the message. You encrypt the entire thing with AES-256 using a random IV that you prepend to the message.

When using AES in CBC (Cipher-block chaining) mode each ciphertext block depends on all plaintext blocks processed up to that point. That is why you also need a random IV (Initialization vector) for the first block.

If you tamper with data it has an effect on all the next blocks. When adding a HMAC with the size of a block to the end of the message one would think that this would provide good security. Or as Nick is saying on Scientopia:

Couldn’t you simply append a secure hash of the preceding plaintext to each message? Unless the attacker can predict the effect of his changes (which shouldn’t be possible), he shouldn’t be able to modify the message _and_ the hash in such a way that it remains undetectable, right?

On the other hand Nate from Root Labs is saying:

For schemes like CBC where there is some redundancy, there are a number of cases where the propagation behavior of CBC does not prevent an attack.

I am not a cryptographer, so I cannot argue why the above schemes are wrong (this article might). I did learn that the correct way of applying the HMAC is in fact very simple: First you apply the AES encryption and then you apply the HMAC independently on the IV and crypted message to ensure neither has been tampered with. It looks like this:

Cipher-Text = IV || AES-256-Encrypt(Plain-Text)
Message = Cipher-Text || SHA-256-HMAC(Cipher-Text)

This scheme has the downside that you will need two keys: one for the HMAC and one for the AES encryption. Note that those keys should be unrelated. To illustrate how the correct scheme works I implemented it (in PHP) below:

// define your key,use length 32,48 or 64 bytes for AES-128,AES-192 and AES-256
$key1 = pack("H*","7206ea04845d1f740401a6e6b2e52d22ac2d314a3d138eb8f61ebe5fb1d6aa5d");
// define the key for the SHA-256 HMAC (both keys should be random shared secrets)
$key2 = pack("H*","dee5cb4a2c4b5b61a729337fc9153daf6387a54b765877752ca36b44a721f469");

// hash comparison function to avoid timing (side channel) attacks
function hash_equals($a, $b) {
  if (!is_string($a) || !is_string($b)) return false;
  if (strlen($a) !== strlen($b)) return false;
  $match = 0;
  for ($i = 0; $i < strlen($a); $i++) {
    $match |= ord($a[$i]) ^ ord($b[$i]);
  return $match===0;

// define the plain text
$text = "123";
// get the block size for the padding
$block_size = mcrypt_get_block_size(MCRYPT_RIJNDAEL_128,MCRYPT_MODE_CBC);
// get the length of the padding needed
$pad = $block_size - (strlen($text) % $block_size);
// pad the plain text with the padding character using PKCS7 padding
$text .= str_repeat(chr($pad),$pad);
// get the size of the initialization vector
$iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128,MCRYPT_MODE_CBC);
// create a random initialization vector
$iv = mcrypt_create_iv($iv_size,MCRYPT_DEV_URANDOM);
// encode the plain text to get the cipher text
$crypt = mcrypt_encrypt(MCRYPT_RIJNDAEL_128,$key1,$text,MCRYPT_MODE_CBC,$iv);
// calculate the SHA-256 HMAC over the initialization vector + cipher text
$hmac = hash_hmac("sha256",$iv.$crypt,$key2,true);
// add the SHA-256 HMAC to the message
$output = bin2hex($iv.$crypt.$hmac);
// print output displaying the process
echo "$text\n";

// tamper with the data
echo "$output\n";
$output = bin2hex(chr(ord(pack("H*",substr($output,0,2))) ^ 1<<3)).substr($output,2);
echo "$output\n";

// read the data back
$message = pack("H*",$output);
// get the size of the initialization vector
$iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128,MCRYPT_MODE_CBC);
// extract the initialization vector from the message
$iv = substr($message,0,$iv_size);
// get the size of the SHA-256 HMAC
$hmac_size = strlen(hash_hmac("sha256","","",true));
// extract the SHA-256 HMAC from the message
$hmac = substr($message,-$hmac_size);
// extract the cipher text from the message
$crypt = substr($message,$iv_size,-$hmac_size);
// calculate the correct SHA-256 HMAC
$crypt_hmac = hash_hmac("sha256",$iv.$crypt,$key2,true);
// check whether the HMACs are equal
if (!hash_equals($hmac,$crypt_hmac)) die("HMAC ERROR\n");
// decode the cipher text to get the plain text
$text = mcrypt_decrypt(MCRYPT_RIJNDAEL_128,$key1,$crypt,MCRYPT_MODE_CBC,$iv);
// get the length of the plain text padding
$pad = ord($text[strlen($text)-1]);
// remove the PKCS7 padding from the plain text
$text = substr($text,0,-1*$pad);
// print output displaying the process
echo "$text\n";

The output of the above code is:


Or if you comment out the tamper line (41):


Leave a Reply