Thursday, 11 April 2013

Making AES256 encryption work the same in PHP as .Net

Further to my previous post, I had data which I had encrypted using AES256 and which I needed to decrypt in PHP. This made me shudder when I realised that I had used a third-party library in .Net and which could do a load of things that might not be possible in PHP. Fortunately, these things really are standard and I'm please to report that it was possible to do this in PHP.

The examples here are only for decryption since I do all the encryption in .Net but the basic principle is the same. This is the function I eventually used:

protected function Decrypt($ciphertext)
{
    try
    {
        $key = $this->pbkdf2(Microsoft_WindowsAzure_RoleEnvironment::getConfigurationSettingValue("Password"),
                Microsoft_WindowsAzure_RoleEnvironment::getConfigurationSettingValue("Salt"),1000,32);
        $cipherTextBytes = base64_decode($ciphertext);
        $ivBytes = substr($cipherTextBytes,0,16);
        $cipherTextBytesOnly = substr($cipherTextBytes,16);
        $decrypted = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $cipherTextBytesOnly, MCRYPT_MODE_CBC, $ivBytes);
        # mcrypt pads the decrypted data with a character whose value is equal to the number of padding characters added
        $dec_s = strlen($decrypted); 
        $padding = ord($decrypted[$dec_s-1]); 
        if ( $padding <= 16 )
        {
            # string might not be padded
            return substr($decrypted, 0, -$padding); 
        }
        else
        {
            return $decrypted;
        }
        
    }
    catch (Exception $e)
    {
        return null;
    }
}

Firstly, a correct key must be derived, in my case from a password and the master salt which are configuration variables in Azure (this password/salt system is based on the owasp implementation of ESAPI). The method being called is shown below and matches the functionality of the .Net Rfc2898DeriveBytes class.

private function pbkdf2( $p, $s, $c, $kl, $a = 'sha1' ) {
    $hl = strlen(hash($a, null, true)); # Hash length
    $kb = ceil($kl / $hl);              # Key blocks to compute
    $dk = '';                           # Derived key

    # Create key
    for ( $block = 1; $block <= $kb; $block ++ ) {
        # Initial hash for this block
        $ib = $b = hash_hmac($a, $s . pack('N', $block), $p, true);
        # Perform block iterations
        for ( $i = 1; $i < $c; $i ++ )
            $ib ^= ($b = hash_hmac($a, $b, $p, true));  # XOR each iteration

        $dk .= $ib; # Append iterated block
    }
    return substr($dk, 0, $kl);
}

I did not write the pbkdf2 function but found it on a github demo project (thanks!)

The original encrypted data is base64 encoded to allow it to be sent over non-binary safe channels so firstly this is reversed. Then, the standard way to store the initialisation vector is as the first x bytes of the stored data - 16 in the case of AES256. This is taken from the front of the data and the remainder becomes the actual encrypted data.

The function then uses mcrypt_decrypt using the RIJNDAEL_128 algorithm. This is because the 128 here is the block size of the cipher, whereas the 256 in the AES is the key length (in bits).

Another by-product of using mcrypt_decrypt is the use of padding in PKCS7 format. This means that the decrypted data will be padded to the next 16 byte block using a character whose ascii code is the same as the number of padding characters that have been added so "help" would be decrypted as "help\f\f\f\f\f\f\f\f\f\f\f\f" since \f is 12 in ascii form. The remainder of the function then checks that the last digit is a padding character (<= 16) and if it is, removes these from the decrypted string. There is the possibility if you are decrypting binary data that the last character of data is actually less than 0x10 which would make the padding code think there are padding characters to remove but this won't happen with strings (which is what I am using).
Post a Comment