3

I want to make an encrypted communication system with a dynamic key exchange between C# and PHP. At the moment I have a working encrypt/decrypt PHP <-> PHP and C# <-> C# but it's not working PHP <-> C# for some reason. The problem is that the C# code decrypted string generates a longer output than the PHP would expect, but the data is the same when viewing the output as a simple string. E.g a string "daa" sent from C# to PHP, decrypted length is 28, which is not what is supposed to be. Same goes with the string sent from PHP to C#, I get a compiler error ArgumentException: length

C# code:

public static string EncryptTest(string input)
{
    string key = "256 bit key (32 char)";

    input = Md5Sum(input).Substring(0, 4) + input;

    var encoding = new UTF8Encoding();
    var Key = encoding.GetBytes(key);
    byte[] encrypted;
    byte[] result;

    using (var rj = new RijndaelManaged())
    {
        try
        {
            rj.Padding = PaddingMode.PKCS7;
            rj.Mode = CipherMode.CBC;
            rj.KeySize = 256;
            rj.BlockSize = 256;
            rj.Key = Key;
            rj.GenerateIV();

            using (ICryptoTransform encryptor = rj.CreateEncryptor())
            {
                using (MemoryStream ms = new MemoryStream())
                {
                    using (CryptoStream cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
                    {
                        using (StreamWriter writer = new StreamWriter(cs))
                        {
                            writer.Write(input);
                        }
                    }

                    encrypted = ms.ToArray();
                    result = new byte[rj.BlockSize / 8 + encrypted.Length];

                    // Result is built as: IV (plain text) + Encrypted(data)
                    Array.Copy(rj.IV, result, rj.BlockSize / 8);
                    Array.Copy(encrypted, 0, result, rj.BlockSize / 8, encrypted.Length);
                }
            }
        }
        finally
        {
            rj.Clear();
        }
    }
    return Convert.ToBase64String(result);
}

public static string DecryptTest(string input)
{
    string key = "256 bit key (32 char)";

    byte[] data = Convert.FromBase64String(input);

    if (data.Length < 32)
        return null;

    var encoding = new UTF8Encoding();
    var Key = encoding.GetBytes(key);

    using (RijndaelManaged aes = new RijndaelManaged())
    {
        aes.Padding = PaddingMode.PKCS7;
        aes.Mode = CipherMode.CBC;
        aes.KeySize = 256;
        aes.BlockSize = 256;
        aes.Key = Key;

        // Extract the IV from the data first.
        byte[] iv = new byte[aes.BlockSize / 8];
        Array.Copy(data, iv, iv.Length);
        aes.IV = iv;

        // The remainder of the data is the encrypted data we care about.
        byte[] encryptedData = new byte[data.Length - iv.Length];
        Array.Copy(data, iv.Length, encryptedData, 0, encryptedData.Length);

        using (ICryptoTransform decryptor = aes.CreateDecryptor())
        {
            using (MemoryStream ms = new MemoryStream(encryptedData))
            {
                using (CryptoStream cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read))
                {
                    using (StreamReader reader = new StreamReader(cs))
                    {
                        string output = reader.ReadToEnd();

                        if (output.Length < 4)
                            return null;

                        string dataHash = output.Substring(0, 4);
                        string dataInput = output.Substring(4);
                        string dataInputHash = Md5Sum(dataInput).Substring(0, 4);

                        if (dataHash != dataInputHash)
                            return null;

                        return dataInput;
                    }
                }
            }
        }
    }
}

private static string Md5Sum(string strToEncrypt)
{
    UTF8Encoding ue = new UTF8Encoding();
    byte[] bytes = ue.GetBytes(strToEncrypt);

    MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider();
    byte[] hashBytes = md5.ComputeHash(bytes);

    string hashString = "";

    for (int i = 0; i < hashBytes.Length; i++)
    {
        hashString += Convert.ToString(hashBytes[i], 16).PadLeft(2, '0');
    }

    return hashString.PadLeft(32, '0');
}

PHP code:

$key = "256 bit key (32 char)";

function iv()
{
    $iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC);
    return mcrypt_create_iv($iv_size, MCRYPT_RAND);
}
function encrypt($data, $key32)
{
    # Prepend 4-chars data hash to the data itself for validation after decryption
    $data = substr(md5($data), 0, 4).$data;
    # Prepend $iv to decrypted data
    $iv = iv();
    $enc = $iv.mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $key32, $data, MCRYPT_MODE_CBC, $iv);
    return base64_encode($enc);
}
function decrypt($data, $key32)
{
    $data = base64_decode($data);
    if ($data === false || strlen($data) < 32)
        return null;
    $iv = substr($data, 0, 32);
    $encrypted = substr($data, 32);
    $decrypted = rtrim(mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $key32, $encrypted, MCRYPT_MODE_CBC, $iv), "\0");
    if ($decrypted === false || is_null($decrypted) || strlen($decrypted) < 4)
        return null;
    $dataHash = substr($decrypted, 0, 4);
    $data = substr($decrypted, 4);
    if (substr(md5($data), 0, 4) !== $dataHash)
        return null; // it breaks here, md5 sum is not correct because of the length
    return $data;
} 
slavacademy
  • 676
  • 1
  • 9
  • 22
  • `DecryptTest` is using a different key, but I'm assuming that's something you forgot to edit. – Thom Wiggers Dec 21 '14 at 10:58
  • Yeah, my bad. All functions are using the same 256 bit key. – slavacademy Dec 21 '14 at 10:59
  • You are not using AES actually, you are using Rijndael with a block size of 256 bit. On both sides though, so it's not the solution. – Maarten Bodewes Dec 21 '14 at 11:57
  • @MaartenBodewes-owlstead But the data IS encrypted/decrypted correctly, all it fails at is the byte length in C#, which I don't know how to solve. – slavacademy Dec 21 '14 at 12:02
  • I'm already writing an answer, don't rush me :) – Maarten Bodewes Dec 21 '14 at 12:03
  • 1
    Be warned that to use a transport protocol that you should also protect your IV & ciphertext *especially CBC ciphertext* for integrity and authenticity (e.g. using a HMAC tag). Otherwise an attacker can alter the plaintext or - worse - completely break confidentiality using a padding Oracle attack. – Maarten Bodewes Dec 21 '14 at 12:13

1 Answers1

2

PHP / mcrypt is not using PKCS#7 padding, it uses zero padding of 0..n bytes where n is the block size.


To PKCS#7-pad the plaintext before encryption, use:

$pad = $blockSize - (strlen($data) % $blockSize);
$pdata = $data . str_repeat(chr($pad), $pad);

to unpad the plaintext after decryption simply perform:

$pad = ord($pdata[strlen($pdata) - 1]);
$data = substr($pdata, 0, strlen($pdata) - $pad);

PKCS#7 is now the ad hoc standard for padding. Zero padding is non-deterministic; it may alter the plaintext if the plaintext ends with zero's itself.

Maarten Bodewes
  • 90,524
  • 13
  • 150
  • 263
  • All combinations work now, except PHP -> C# decrypt. When I try to decrypt in C# a string encrypted in PHP, I get the following exception: *CryptographicException: Bad PKCS7 padding. Invalid length 126. Mono.Security.Cryptography.SymmetricTransform.ThrowBadPaddingException* Any idea why? – slavacademy Dec 21 '14 at 13:24
  • That should be block size in bytes, sorry, trying to create a function with PHPDoc in the mean time... – Maarten Bodewes Dec 21 '14 at 13:31
  • I'm using this in PHP encrypt for block size *$blockSize = mcrypt_get_block_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC);* – slavacademy Dec 21 '14 at 13:37
  • You are doing the padding *before* encryption in PHP, right? – Maarten Bodewes Dec 21 '14 at 13:40
  • I tried to use a try..finally block, and used aes.Clear() in the finally, but there was the same result. I have edited my question to put my current C# code for decrypting, maybe I'm missing something? – slavacademy Dec 21 '14 at 13:43
  • And I'm doing the padding in PHP before encryption. As I mentioned, all combinations work now(c# -> php, php -> php, c# -> c#) except php -> c# – slavacademy Dec 21 '14 at 13:44
  • So, basically, the PHP code can successfully decrypt both base64 strings, but the C# code can only decrypt base64 strings that were created by its own decryptor. – slavacademy Dec 21 '14 at 13:51
  • Hmm, the padding seems to succeed, note that a single bit mistake in IV, ciphertext or key can already lead to padding errors. So compare all your input / output by printing out hex strings... – Maarten Bodewes Dec 21 '14 at 13:55
  • I've created and tested the padding functions just posted [here](http://stackoverflow.com/a/27590539/589259) – Maarten Bodewes Dec 21 '14 at 15:00