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.
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
.
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?"