views:

546

answers:

1

This question is a continuation of my last one, regarding How to make Ruby AES-256-CBC and PHP MCRYPT_RIJNDAEL_128 play well together. I've got that working now, but I'm still struggling to go the other direction. The PHP generated cryptogram appears to have all the information that was provided, but I cannot get the Ruby code to decrypt it without error.

Here's the PHP code I'm using to generate the cryptogram:

$cleartext = "Who's the clever boy?";
$key = base64_decode("6sEwMG/aKdBk5Fa2rR6vVw==\n");
$iv = base64_decode("vCkaypm5tPmtP3TF7aWrug==");
$cryptogram = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $cleartext, MCRYPT_MODE_CBC, $iv);
$result = base64_encode($cryptogram);
print "\n'$result'\n";

RESULT
'JM0OxMINPTnF1vwXdI3XdKI0KlVx210CvpJllFja+GM='

Then here's the attempt to decrypt in Ruby:

>> cipher = OpenSSL::Cipher::Cipher.new('aes-128-cbc')
>> cipher.key = Base64.decode64("6sEwMG/aKdBk5Fa2rR6vVw==\n")
>> cipher.iv = Base64.decode64("vCkaypm5tPmtP3TF7aWrug==")
>> cryptogram = Base64.decode64('JM0OxMINPTnF1vwXdI3XdKI0KlVx210CvpJllFja+GM=')
>> cleartext = cipher.update(cryptogram)
=> "Who's the clever"
>> cleartext << cipher.final
OpenSSL::Cipher::CipherError: bad decrypt
 from (irb):100:in `final'
 from (irb):100

What's really frustrating about this is that it's possible to get the entire cleartext out of that encrypted string. Repeating the above, but adding a nonsense pad to the cryptogram:

  >> cleartext = cipher.update(cryptogram + 'pad')
  => "Who's the clever boy?\000\000\000\000\000\000\000\000\000\000\000"
  >> cleartext << cipher.final
  OpenSSL::Cipher::CipherError: bad decrypt
   from (irb):119:in `final'
   from (irb):119

In my actual use case the cleartext is structured (a JSON string, since you ask), so I feel comfortable a this point that I could tell use this scheme and detect poorly encrypted input without performing the cipher.final. However, I can't tolerate this sort of kludge in my code, so I'd like to understand how to make the ruby code handle the final block gracefully.

+3  A: 

The problem is that mcrypt isn't padding the last block, whereas Ruby's OpenSSL binding uses the default OpenSSL padding method, which is PKCS padding. I can't really improve on the description from the OpenSSL documentation:

PKCS padding works by adding n padding bytes of value n to make the total length of the data a multiple of the block size. Padding is always added so if the data is already a multiple of the block size n will equal the block size. For example if the block size is 8 and 11 bytes are to be encrypted then 5 padding bytes of value 5 will be added.

You'll need to manually add proper padding to the end of the cleartext in PHP before encrypting. To do that, pass your $cleartext through this pkcs5_pad function on the PHP side before you encrypt it (passing 16 as the blocksize).

function pkcs5_pad ($text, $blocksize)
{
    $pad = $blocksize - (strlen($text) % $blocksize);
    return $text . str_repeat(chr($pad), $pad);
}

If you also go the other way (encrypt in Ruby and decrypt with mcrypt), you'll have to strip off the padding bytes after decrypting.

Side note: The reason you have to add padding even if the cleartext is already a multiple of the blocksize (a whole block of padding), is so that when you are decrypting you know that the last byte of the last block is always the amount of padding added. Otherwise, you couldn't tell the difference between cleartext with a single padding byte and a cleartext with no padding bytes that just happened to end in the value 0x01.

caf
Thanks for your thoughts, caf. Adding text to the cleartext is just creating a different string to encrypt; it doesn't change the result.I agree that the problem has something to do with how the two implementations deal with the last block of the encrypted stream. The output of the two algorithms is identical until the last 32-byte block, where the last 16 bytes are completely different. I've run out of patience with the problem, so unless some samaritan comes by and solves it for me, I'm going with the kludge above.
The text added to the end of the cleartext (padding) has to be of a very specific form, that the Ruby side is expecting. I will investigate what padding method it uses and update the answer.
caf
...PKCS padding works by adding n padding bytes of value n to make the total length of the **encrypted** data a multiple of the block size...
Although on reflection that wording is a little confusing, padding is most *definitely* added to the unencrypted data.
caf
caf, I owe you, starting with an apology and continuing through a virtual tasty beverage of your choice (if you're anywhere near Seattle, the beverage can be non-virtual). Mind-boggling as it is to me, you were spot on and padding the source -- even if it's exactly 0 mod 16 in length -- is required. Thank you.
Glad you got it working! I've updated the answer in case it helps someone else with the same problem. (I've also answered your question about why padding is needed even if the source is already a multiple of the blocksize).
caf