2

I am trying to convert the following php code to C#:

 $m_params = urlencode(base64_encode(mcrypt_encrypt(MCRYPT_RIJNDAEL_256,$key, json_encode($arParams), MCRYPT_MODE_ECB)));

What the documentation says:

m_params : A JSON array of data of additional parameters encrypted using the Rijindael-256 algorithm and encoded using a base64 algorithm.

What I've assumed?

Step 1: Create an array of params i.e. $arParams For php its declared like:

$arParams = array(
'success_url' => 'http://google.com/new_success_url',
'fail_url' => 'http://google.com/new_fail_url',
'status_url' => 'http://google.com/new_status_url',
);

For C# I've declared it like this:

 var additional_params = new object[]
                        {
                            new {"http://google.com/new_success_url"},
                            new {"http://google.com/new_fail_url"},
                            new {"http://google.com/new_status_url"},
                        };

Step 2: Encode to JSON string, I've used JsonConvert.SerializeObject(additional_params);

Step 3: Encrypt the result using RIJNDAEL-256 Algorithm using ECB (I've used CBC as well)

Step 4: Encode the result using base64. I've used Convert.ToBase64String(encrypted);

Step 5: Url encode the result. I've used HttpUtility.UrlEncode(base64String, Encoding.UTF8);

Step 6: Save the result in m_params

My current code looks like this:

                var additional_params = new object[]
                    {
                        new {"http://google.com/new_success_url"},
                        new {"http://google.com/new_fail_url"},
                        new {"http://google.com/new_status_url"},
                    };
                string m_params ="";
                //converting to Json object additional params
                var jsonEncoded = JsonConvert.SerializeObject(additional_params);
                try
                {

                    string original = jsonEncoded;

                    // Create a new instance of the RijndaelManaged
                    // class.  This generates a new key and initialization
                    // vector (IV).
                    using (RijndaelManaged myRijndael = new RijndaelManaged())
                    {
                        var final_Key = CreateMD5(payeer.m_key + payeer.m_orderid);
                        var rfc = CreateKey(final_Key);
                        
                        // Encrypt the string to an array of bytes.
                        byte[] encrypted = EncryptStringToBytes(original, rfc[0], rfc[1]);
                        var base64String = Convert.ToBase64String(encrypted);
                        m_params = HttpUtility.UrlEncode(base64String, Encoding.UTF8);
                        // Decrypt the bytes to a string.
                        string roundtrip = DecryptStringFromBytes(encrypted, rfc[0], rfc[1]);

                        //Display the original data and the decrypted data.
                        Console.WriteLine("Original:   {0}", original);
                        Console.WriteLine("Round Trip: {0}", roundtrip);
                    }

         static byte[] EncryptStringToBytes(string plainText, byte[] Key, byte[] IV)
           {
            // Check arguments.
            if (plainText == null || plainText.Length <= 0)
                throw new ArgumentNullException("plainText");
            if (Key == null || Key.Length <= 0)
                throw new ArgumentNullException("Key");
            if (IV == null || IV.Length <= 0)
                throw new ArgumentNullException("IV");
            byte[] encrypted;
            // Create an RijndaelManaged object
            // with the specified key and IV.
            using (RijndaelManaged rijAlg = new RijndaelManaged())
            {
                rijAlg.Key = Key;
                rijAlg.IV = IV;
                rijAlg.Mode = CipherMode.ECB;
              //  rijAlg.KeySize = 256;
                rijAlg.BlockSize = 256;
                rijAlg.Padding = PaddingMode.PKCS7;
                // Create an encryptor to perform the stream transform.
                ICryptoTransform encryptor = rijAlg.CreateEncryptor(rijAlg.Key, rijAlg.IV);

                // Create the streams used for encryption.
                using (MemoryStream msEncrypt = new MemoryStream())
                {
                    using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
                    {
                        using (StreamWriter swEncrypt = new StreamWriter(csEncrypt))
                        {

                            //Write all data to the stream.
                            swEncrypt.Write(plainText);
                        }
                        encrypted = msEncrypt.ToArray();
                    }
                }
            }

            // Return the encrypted bytes from the memory stream.
            return encrypted;
        }


public static string CreateMD5(string input)
        {
            // Use input string to calculate MD5 hash
            using (System.Security.Cryptography.MD5 md5 = System.Security.Cryptography.MD5.Create())
            {
                byte[] inputBytes = System.Text.Encoding.ASCII.GetBytes(input);
                byte[] hashBytes = md5.ComputeHash(inputBytes);

                // Convert the byte array to hexadecimal string
                StringBuilder sb = new StringBuilder();
                for (int i = 0; i < hashBytes.Length; i++)
                {
                    sb.Append(hashBytes[i].ToString("X2"));
                }
                return sb.ToString();
            }
        }
        public static dynamic CreateKey(string password)
        {
            var salt = new byte[] { 1, 2, 23, 234, 37, 48, 134, 63, 248, 4 };

            const int Iterations = 9872;
            using (var rfc2898DeriveBytes = new Rfc2898DeriveBytes(password, salt, Iterations))
            {
                var key = rfc2898DeriveBytes.GetBytes(32);
                var IV = rfc2898DeriveBytes.GetBytes(16);
                dynamic[] arr = new dynamic[2];
                arr[0] = key;
                arr[1] = IV;
                return arr;
            }
                
        }

Its not giving the same output. Am I missing something??

Umer Khalid
  • 99
  • 10
  • Does decrypt work? Is public key same number of bytes? Not sure if issue is the padding mode or just that public key is different. You have a public and private key. The private key needs to be the same when encrypting and decrypting and doesn't get transmitted. The public key gets transmitted and is a hash of the actual data. So if message gets decrypt then the data is different. If the message does not get decrypted then the algorithm is different which includes the padding mode. c# and java default padding mode is different. – jdweng Dec 30 '20 at 13:22
  • @jdweng: Once again I must note that there is **no** public or private key in this problem. There is only a symmetric key. – President James K. Polk Dec 30 '20 at 13:41
  • 1
    You PHP code uses Rijndael with the 256-bit block size; this is not AES. See this comment in the .Net documentation for `RijndaelManaged`: *This algorithm supports key lengths of 128, 192, or 256 bits; defaulting to 256 bits. In .NET Framework, this algorithm supports block sizes of 128, 192, or 256 bits; defaulting to 128 bits (Aes-compatible). In .NET Core, it is the same as AES and supports only a 128-bit block size.* Thus in .Net Framework you must explicitly set the `BlockSize` to 256. In .Net core you're out of luck and must use a third-party library. – President James K. Polk Dec 30 '20 at 13:44
  • 1
    The paddings of both codes differ. `mcrypt` in the PHP code uses Zero padding, in the C# code PKCS7 is applied which therefore needs to be changed to `PaddingMode.Zeros` (although PKCS7 is the more reliable padding). Why do you need the methods `CreateMD5` and `CreateKey`? In the PHP code no key is derived. Apart from that, the corresponding definitions are missing in your post. – Topaco Dec 30 '20 at 15:00
  • In the `additional_params` instantiation the variable names (`success_url` etc.) are not contained and therefore not in the serialization. More useful here is e.g. an anonymous type like `var additional_params = new {success_url = "...", fail_url = "...", status_url = "..."};` For serialization, note that in PHP / is often serialized as \/, while in C# it is simply serialized as /, but this can be corrected manually (e.g. with `Replace("/", "\\/")`). – Topaco Dec 30 '20 at 15:06
  • @jdweng yes, so decrypt works and does give out the original string. But that's not the problem here. The problem is why I'm not getting the same output as the php code – Umer Khalid Dec 30 '20 at 18:25
  • @PresidentJamesK.Polk If you see my code again, you will notice this line, `rijAlg.BlockSize = 256;` . So I AM explicitly setting the block size to 256. – Umer Khalid Dec 30 '20 at 18:29
  • @Topaco I will try using `Padding.Zero`. Plus I have edited my code to include the definitions of `CreateMD5` and `CreateKey`. So basically the php code which references this is `$key = md5('Key for encrypting additional parameters'.$m_orderid);` which I have converted to C# – Umer Khalid Dec 30 '20 at 18:38
  • @Topaco Nice find, about the `additional_params`. How could I have left that. How silly of me. Let me do that and if it all works out, I will post out your answer as the correct one for any new comers. – Umer Khalid Dec 30 '20 at 18:42
  • I was looking at `using (RijndaelManaged myRijndael = new RijndaelManaged())` but I see now you don't even use that. That just makes it confusing to read. – President James K. Polk Dec 30 '20 at 19:06

1 Answers1

4

As mentioned in President James K. Polk's comment, Rijndael with a block size of 256 bits is only supported in the .NET Framework, not in .NET Core. You did not specify the version you are running, but since you use a block size of 256 bits in the posted code (rijAlg.BlockSize = 256;), I assume you are running .NET Framework (otherwise, you need to apply a third party library that supports Rijndael with a block size of 256 bits, such as BouncyCastle/C#).

Both codes use a different padding. mcrypt applies Zero padding by default, the C# code explicitly uses PKCS7 padding (which is also the C# default). So that the C# code provides the same result as the PHP code, it is necessary to switch to Zero padding in the C# code (it should be noted that Zero padding is unreliable, unlike PKCS7 padding).

When additional_params is instantiated (which, by the way, does not compile on my machine), the variable names are missing, so they are also missing in the serialization. An anonymous type could be used instead. Also, note that json_encode() escapes the slash (/) by default, i.e. converts it to a \/, which has to be done manually in the C# code, e.g. with Replace("/", "\\/"). One possible implementation of the JSON serialization is:

using Newtonsoft.Json;
...
var additionalParams = new
{
    success_url = "http://google.com/new_success_url",
    fail_url = "http://google.com/new_fail_url",
    status_url = "http://google.com/new_status_url"
};
string jsonEncoded = JsonConvert.SerializeObject(additionalParams).Replace("/", "\\/");

In the PHP code, the key is derived from a password using the MD5 digest. By default, md5() returns the result as a hexadecimal string, which converts the 16 bytes hash into a 32 bytes value that is applied as the key, so that AES-256 is used. PHP represents the hexadecimal digits with lowercase letters, which must also be implemented accordingly in the C# code, e.g.:

using System;
using System.Text;
using System.Security.Cryptography;
...
MD5 md5 = MD5.Create();
string password = "My password"; // test password 
byte[] passwordHash = md5.ComputeHash(Encoding.UTF8.GetBytes(password));
string passwordHashHex = BitConverter.ToString(passwordHash).Replace("-", "").ToLower();  // convert byte array to lowercase hex string as in PHP
byte[] key = Encoding.UTF8.GetBytes(passwordHashHex);

where the conversion of the byte array to the hexadecimal string is done with BitConverter, see here.

A possible implementation for the encryption is:

using System;
using System.IO;
using System.Web;
using System.Text;
using System.Security.Cryptography;
...
byte[] encrypted = null;
using (RijndaelManaged rijndael = new RijndaelManaged())
{
    rijndael.Key = key;
    rijndael.Mode = CipherMode.ECB;           // default: CBC
    rijndael.BlockSize = 256;                 // default: 128
    rijndael.Padding = PaddingMode.Zeros;     // default: PKCS7

    ICryptoTransform encryptor = rijndael.CreateEncryptor(rijndael.Key, null);
    using (MemoryStream msEncrypt = new MemoryStream())
    {
        using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
        {
            using (StreamWriter swEncrypt = new StreamWriter(csEncrypt))
            {
                swEncrypt.Write(jsonEncoded);
            }
            encrypted = msEncrypt.ToArray();
        }
    }
}
string base64String = Convert.ToBase64String(encrypted);
string m_params = HttpUtility.UrlEncode(base64String, Encoding.UTF8);
Console.WriteLine(m_params);

where this code with the used test password gives the following result:

C3pldgsLDSqfG28cbt%2fv0uiBNQT6cWn86iRwg%2bv2blTzR7Lsnra%2b2Ok35Ex9f9UbG%2bjhKgITUQ8kO3DrIrWUQWirzYzwGBucHNRThADf60rGUIBDdjZ2kOIhDVXUzlMsZtBvYIgFoIqFJXCbhZq9GGnKtABUOa5pcmIYeUn%2b%2fqG1mdtJenP5vt8n0eTxsAd6CFc1%2bguR0wZx%2fEZAMsBBRw%3d%3d

in accordance with the result of the following PHP code:

$key = md5('My password'); // test password
$arParams = array(
    'success_url' => 'http://google.com/new_success_url',
    'fail_url' => 'http://google.com/new_fail_url',
    'status_url' => 'http://google.com/new_status_url',
);
$m_params = urlencode(base64_encode(mcrypt_encrypt(MCRYPT_RIJNDAEL_256,$key, json_encode($arParams), MCRYPT_MODE_ECB)));
print($m_params . "\n");

Note that C# uses lowercase letters for the url encoding, while PHP uses uppercase letters, which represents the same url encoding, see RFC 3986, sec. 2.1. If the C# code should nevertheless also apply uppercase letters for the url encoding, this can easily be achieved using regular expressions, see e.g. here.


A few remarks regarding security:

The PHP code applies the insecure ECB mode. For security reasons, a mode with an IV should be used, e.g. CBC or GCM. The latter provides implicit authenticated encryption. The IV is randomly generated for each encryption, is not secret and is sent to the recipient along with the ciphertext (usually prepended).

MD5 as a key derivation function (KDF) is also insecure. Here, a reliable KDF should be used, e.g. PBKDF2.

In addition, using the hexadecimal string as the key weakens the same, since each byte is reduced to the 16 values of the hexadecimal number system. More secure is the use of the binary data generated by the KDF, so that each byte can take 256 different values.

mcrypt is deprecated. A possible alternative is openssl.

Community
  • 1
  • 1
Topaco
  • 40,594
  • 4
  • 35
  • 62
  • You are a saint. I was working on it for more than 3 days and was pulling my hair out. The algorithm worked finally! – Umer Khalid Dec 31 '20 at 12:31