将PHP AES 128 ECB加密与C#匹配

I have the following PHP sample that I'm trying to mimic in C#. It is using AES 128bit ECB encryption with PKCS7 padding:

$trust_jsonString="hello";
echo "input: '" . $trust_jsonString . "'
";
echo "input (dump): " . var_dump($trust_jsonString) . "
";

$trust_key = "9840822c-14fc-49ac-9d68-ac532f9f171e";
echo "key: '" . $trust_key . "'
";

$blockSize=mcrypt_get_block_size(MCRYPT_RIJNDAEL_128,MCRYPT_MODE_ECB);
echo "block size: '" . $blockSize . "'
";

$padding = $blockSize - (strlen($trust_jsonString) % $blockSize);
echo "padding: '" . $padding . "'
";

$trust_jsonString .= str_repeat(chr($padding), $padding);

$trust_jsonString = utf8_encode($trust_jsonString);
echo "utf8 json: '" . $trust_jsonString . "'
";
echo "utf8 json (dump): " . var_dump($trust_jsonString) . "
";

$trust_key=utf8_encode($trust_key);
echo "encoded key: " . $trust_key . "
";

$trust_key=(md5($trust_key));
echo "md5 hash of key (raw): " . $trust_key . "
";

$mcrypt = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $trust_key, $trust_jsonString, MCRYPT_MODE_ECB);
echo "mcrypt (raw): " . var_dump($mcrypt) . "
";
echo "mcrypt: " . $mcrypt . "
";

echo "mcrypt (raw): ";
$byte_array = byteStr2byteArray($mcrypt);
for($i=0;$i<count($byte_array);$i++)
{
   printf("%02x", $byte_array[$i]);
}
echo "
";

$presid = base64_encode($mcrypt);
echo "presid: " . $presid . "
";

$sid=strtr($presid,'+/', '-_');
echo "sid: " . $sid . "
";

function byteStr2byteArray($s) {
    return array_slice(unpack("C*", "\0".$s), 1);
}

I am currently running the following C# code to try to mimic results:

static void Main( string[] args )
{
    string data = "hello";
    Encrypt(data);
}

static void Encrypt( string data )
{
    PaddingMode padding = PaddingMode.PKCS7;
    CipherMode cipherMode = CipherMode.ECB;
    int size = 128;

    Console.WriteLine("input: '" + data + "'");

    string officialKey = "9840822c-14fc-49ac-9d68-ac532f9f171e";
    Console.WriteLine("key: '" + officialKey + "'");

    Console.WriteLine("block size: '16'");
    Console.WriteLine( "padding: '11'" );

    var utf8dataBytes = Encoding.UTF8.GetBytes(data);
    var utf8data = Encoding.UTF8.GetString(utf8dataBytes);
    Console.WriteLine("utf8 json: '" + utf8data + "'");

    Console.WriteLine("encoded key (utf8): " + officialKey);

    var utf8KeyBytes = Encoding.UTF8.GetBytes(officialKey);

    var myMD5 = MD5.Create();
    var md5HashOfKey = myMD5.ComputeHash(utf8KeyBytes);

    Console.WriteLine( "md5 hash of key (raw): " + DumpBinary(md5HashOfKey) );

    byte[] encryptedBlob;

    using ( var aes = new AesManaged() )
    {
        try
        {
            aes.Padding = PaddingMode.PKCS7;
            aes.Mode = CipherMode.ECB;
            aes.KeySize = 128;
            aes.BlockSize = 128;
            aes.Key = md5HashOfKey;
            //aes.IV = new byte[] { 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0 };

            var bytes = utf8dataBytes;
            //var bytes = ForcePaddingManually( utf8dataBytes );

            var cxform = aes.CreateEncryptor();
            encryptedBlob = cxform.TransformFinalBlock( bytes, 0, bytes.Length );
        }
        finally
        {
            aes.Clear();
        }
    }

    //var encryptedBlob = AesAlgo.Encrypt( data, md5HashOfKey, padding, cipherMode, size );
    Console.WriteLine("mcrypt (raw): " + DumpBinary(encryptedBlob));
    Console.WriteLine("mcrypt: " + Encoding.UTF8.GetString(encryptedBlob));

    var encryptedBase64 = Convert.ToBase64String(encryptedBlob);
    Console.WriteLine( "presid: " + encryptedBase64 );

    var encodedEncryptedBlob = encryptedBase64.Replace( "+", "-" ).Replace( "/", "_" );
    Console.WriteLine( "sid: " + encodedEncryptedBlob );
    Console.WriteLine("COMPLETE!");
}

static byte[] ForcePaddingManually( byte[] data )
{
    // force padding manually to test that PKCS7 works like we are expecting
    var pad = ( 16 - data.Length % 16 ) % 16;
    var bytes = new byte[data.Length + pad];
    for ( int i = 0; i < data.Length; ++i )
    {
        bytes[i] = data[i];
    }
    for ( int i = data.Length; i < data.Length + pad; ++i )
    {
        bytes[i] = (byte)pad;
    }

    return bytes;
}

static string DumpBinary( byte[] data )
{
    var sb = new StringBuilder();
    for ( int i = 0; i < data.Length; ++i )
    {
        sb.Append( data[i].ToString( "X2" ) );
    }
    return sb.ToString();
}

When I look at the results for these, this is what I get:

PHP:

input: 'hello'
string(5) "hello"
input (dump): 
key: '9840822c-14fc-49ac-9d68-ac532f9f171e'
block size: '16'
padding: '11'
utf8 json: 'hello'
string(16) "hello"
utf8 json (dump): 
encoded key: 9840822c-14fc-49ac-9d68-ac532f9f171e
md5 hash of key (raw): 6d334201cb7625323da32e0c31b2b138
string(16) "�Ҙ�= �˹��C���"
mcrypt (raw): 
mcrypt: �Ҙ�= �˹��C���
mcrypt (raw): b0d298c83d20a0cbb9f0ea4305cef2ec
presid: sNKYyD0goMu58OpDBc7y7A==
sid: sNKYyD0goMu58OpDBc7y7A==

C#:

input: 'hello'
key: '9840822c-14fc-49ac-9d68-ac532f9f171e'
block size: '16'
padding: '11'
utf8 json: 'hello'
encoded key (utf8): 9840822c-14fc-49ac-9d68-ac532f9f171e
md5 hash of key (raw): 6D334201CB7625323DA32E0C31B2B138
mcrypt (raw): 4CBAD7678AAB2B054371A1B572161280
mcrypt: L??g??+♣Cq??r▬↕?
presid: TLrXZ4qrKwVDcaG1chYSgA==
sid: TLrXZ4qrKwVDcaG1chYSgA==
COMPLETE!

There is a lot of diagnostic and misc code in there, but the basic issue is that when the same MD5 hash of the key is passed in (binaries are the same) and the same input data is passed in (bytes are the same, or I can force the padding to be the same in the C# code on input) I get different output results. I'm sure that this is something simple, but it isn't popping out to me. Is there someone who can identify the problem here?

The basic issue is that the SID shown at the bottom (the AES encrypted result) is different - what is causing the difference?

The main problem is that the actual key in the PHP code is created by md5() without the optional argument to get the raw output. This means that the actual key is 32 bytes long, because it is hex encoded and it is used as-is without decoding. Essentially, you're dealing with AES-256 here.

Since you can't change the PHP code, you need to re-create this mishap in C#:

aes.KeySize = 256;
aes.Key = Encoding.ASCII.GetBytes(BitConverter.ToString(md5HashOfKey).Replace("-","").ToLower());

Output as expected:

mcrypt (raw): B0D298C83D20A0CBB9F0EA4305CEF2EC
mcrypt: �Ҙ�= �˹��C���
presid: sNKYyD0goMu58OpDBc7y7A==
sid: sNKYyD0goMu58OpDBc7y7A==

BitConverter.ToString() returns a hex string of the form "6D-33-42-01-....", which means that the dashes must be removed and it must be converted to lowercase to create the actual key.

DEMO

Well the first thing that would help narrow down problem is the "md5 hash of key (raw):" is identical in both cases, but the base64 encoding doesn't match "md5 hash of key (base64):", so that kind of narrows down the step where the problem diverges to the code for the base64call.

Note your input is a hex string, not binary in PHP.

Testing with an online converter that is specifically for hex strings to base64, I can verify that bTNCAct2JTI9oy4MMbKxOA== is the correct base64 encoding, for the input of 0x6d334201cb7625323da32e0c31b2b138 which matches C#'s output.

So that means that PHP's base64 encoding is the culprit. There's probably a better way to retrieve the key in binary, but what you need to do is convert the hex string into binary:

<?php
echo base64_encode(pack("H*" , '6d334201cb7625323da32e0c31b2b138')) ;
?>

This produces the expected output of bTNCAct2JTI9oy4MMbKxOA==

There could be more problems than that, but that explains that difference in output.