May 12, 2017

Writing C#’s Rfc2898DeriveBytes in PHP

Recently I came across a third party backend writen in C# that encrypted small pieces of sensible data for storing in the database with AES-256-CBC. The piece of code responsible for the encryption and decryption can be found at the bitlush blog (kudos to the authors). This backend would send this encrypted information to our backend system writen in PHP, and it was our responsability to decrypt those pieces of data on our end, and so we had to effectively at least write the decrypt function exactly the same way as the C#’s counterpart. We went on to write a PHP class that is a 1:1 port.

Understanding the C# class

The Encrypt/Decrypt functions are fairly similar, so we can actually study only one of them to understand what needs to be done. Let’s take a look at the first line of the Decrypt method:

DeriveBytes rgb = new Rfc2898DeriveBytes(password, Encoding.Unicode.GetBytes(salt));

The documentation of Rfc2898DeriveBytes states that this class “implements password-based key derivation functionality, PBKDF2, by using a pseudo-random number generator based on HMACSHA1”. This gives us a first clue on where to start: It uses the PBKDF2 derivation function standard with SHA1. Ok, cool, PHP 5.5+ has a built in hash_pbkdf2 method that does exactly this. Problem solved? Not so fast…

Notice the Encoding.Unicode.GetBytes() method? Yup, this one is a tricky one. The salt string is being encoded and transformed to a byte array before being passed onwards to the derivation method. Although we don’t need to convert our salt string to a byte array in PHP we do need to take into consideration the encoding shift done here. The Encoding.Unicode states that it uses a “UTF-16 format using the little endian byte order”. If you’re like me that reads everything too fast you can easily miss the little endian part here. UTF-16 on it’s own won’t make it, we need to make sure we convert our salt string to UTF-16LE. We can use the mb_convert_encoding PHP function to achieve this. Sweet. We can now port the first line of the Decrypt function to PHP (assume $password and $salt are initialized with the respective values):

$AESKeyLength = 32;
$AESIVLength = 16;
$pbkdf2 = hash_pbkdf2("SHA1", $password, mb_convert_encoding($salt, 'UTF-16LE'), 1000, $AESKeyLength + $AESIVLength, TRUE);

Here we use 1000 iterations as it’s the PBKDF2 default value for iterations and 48 (32+16) as the output string length as we need 32 bytes for AES-256 key size and 16 bytes for initialization vector. Finally we set TRUE so that hash_pdfkdf2 returns raw output instead of strings.

Key and Initialization Vector

Next in our C#’s code is the creation of the key and initialization vector based on the derivation string we got on the step before, the code looks like this:

byte[] rgbKey = rgb.GetBytes(algorithm.KeySize >> 3);
byte[] rgbIV = rgb.GetBytes(algorithm.BlockSize >> 3);

Please note again, that in PHP we are not working with byte array, rather with raw values, so a similar way to do this would be:

$key = substr($pbkdf2, 0, $AESKeyLength);
$iv = substr($pbkdf2, $AESKeyLength, $AESIVLength);

We now have the necessary values to proceed with the decryption of the ecnrypted string (assume $message contains the encrypted string). For this we will use the PHP’s openssl implementation.

$decryptedString = openssl_decrypt($message, 'aes-256-cbc', $key, NULL, $iv);

Et voilá, done. You can find a complete PHP class below, that was improved to support also the encrypt method and some other ciphers other than AES-256.