10
votes

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.

2

2 Answers

15
votes

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.

0
votes

It appears PHP \0 pads the cleartext before encrypting it. You can set Ruby to disable the padding.

http://www.ruby-doc.org/stdlib-1.9.3/libdoc/openssl/rdoc/OpenSSL/Cipher.html#method-i-padding-3D

This will work, but then you must strip the padding off manually.

1.9.3p125 :008 > cipher = OpenSSL::Cipher::Cipher.new('aes-128-cbc')
 => #<OpenSSL::Cipher::Cipher:0x0000000561ee78>
1.9.3p125 :009 > cipher.decrypt
 => #<OpenSSL::Cipher::Cipher:0x0000000561ee78>
1.9.3p125 :010 > cipher.padding = 0
 => 0
1.9.3p125 :011 > cipher.key = Base64.decode64("6sEwMG/aKdBk5Fa2rR6vVw==\n")
 => "\xEA\xC100o\xDA)\xD0d\xE4V\xB6\xAD\x1E\xAFW"
1.9.3p125 :012 > cipher.iv = Base64.decode64("vCkaypm5tPmtP3TF7aWrug==")
 => "\xBC)\x1A\xCA\x99\xB9\xB4\xF9\xAD?t\xC5\xED\xA5\xAB\xBA"
1.9.3p125 :013 > cryptogram =  Base64.decode64('JM0OxMINPTnF1vwXdI3XdI2j8NJ8kr+Du0fnkxorNl0=')
 => "$\xCD\x0E\xC4\xC2\r=9\xC5\xD6\xFC\x17t\x8D\xD7t\x8D\xA3\xF0\xD2|\x92\xBF\x83\xBBG\xE7\x93\x1A+6]"
1.9.3p125 :014 > cleartext = cipher.update(cryptogram)
 => "Who's the clever girl?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
1.9.3p125 :015 > cleartext << cipher.final
 => "Who's the clever girl?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"



1.9.3p125 :042 > cleartext.strip
 => "Who's the clever girl?"