Asked  7 Months ago    Answers:  5   Viewed   42 times

UPDATED

I have made the changes to the C# code so it uses a block size of 256. but now the hello world looks like this http://pastebin.com/5sXhMV11 and I cant figure out what I should use with rtrim() to get ride of the mess at the end.

Also when you say the IV should be random, by this do you mean don't use the same IV more then once or is the way I have coded it wrong?

Thanks again!

Hi,

I'm trying to decrypt a string with PHP that was encrypted in C#. I can't seem to get PHP to decrypt it using mcrypt and could do with some help please. I get the following error with php so I am guessing I'm not setting the IV correctly.

Error: The IV parameter must be as long as the blocksize

Both functions use the same cipher, key, IV and set to CBC mode:

encrypted text from c# = UmzUCnAzThH0nMkIuMisqg==
key 32 long = qwertyuiopasdfghjklzxcvbnmqwerty
iv 16 long = 1234567890123456

C#

    public static string EncryptString(string message, string KeyString, string IVString)
    {
        byte[] Key = ASCIIEncoding.UTF8.GetBytes(KeyString);
        byte[] IV = ASCIIEncoding.UTF8.GetBytes(IVString);

        string encrypted = null;
        RijndaelManaged rj = new RijndaelManaged();
        rj.Key = Key;
        rj.IV = IV;
        rj.Mode = CipherMode.CBC;

        try
        {
            MemoryStream ms = new MemoryStream();

            using (CryptoStream cs = new CryptoStream(ms, rj.CreateEncryptor(Key, IV), CryptoStreamMode.Write))
            {
                using (StreamWriter sw = new StreamWriter(cs))
                {
                    sw.Write(message);
                    sw.Close();
                }
                cs.Close();
            }
            byte[] encoded = ms.ToArray();
            encrypted = Convert.ToBase64String(encoded);

            ms.Close();
        }
        catch (CryptographicException e)
        {
            Console.WriteLine("A Cryptographic error occurred: {0}", e.Message);
            return null;
        }
        catch (UnauthorizedAccessException e)
        {
            Console.WriteLine("A file error occurred: {0}", e.Message);
            return null;
        }
        catch (Exception e)
        {
            Console.WriteLine("An error occurred: {0}", e.Message);
        }
        finally
        {
            rj.Clear();
        }

        return encrypted;
    }

PHP

var $mcrypt_cipher = MCRYPT_RIJNDAEL_256;
var $mcrypt_mode = MCRYPT_MODE_CBC;

function decrypt($key, $iv, $encrypted)
{
    $encrypted = base64_decode($encrypted);

    $decrypted = rtrim(mcrypt_decrypt($this->mcrypt_cipher, $key, $encrypted, $this->mcrypt_mode, $iv), "");;
    return $decrypted;
}

Thanks

 Answers

88

If you want to use Rijndael256 in your C# application you have to set the BlockSize to 256.

RijndaelManaged rj = new RijndaelManaged();
rj.BlockSize = 256;

And then your iv has to be 256 bits long as well.
see SymmetricAlgorithm.BlockSize Property


Or the other way round: Currently your C# application uses Rijndael128 and so must your php script.

<?php
class Foo {
  protected $mcrypt_cipher = MCRYPT_RIJNDAEL_128;
  protected $mcrypt_mode = MCRYPT_MODE_CBC;

  public function decrypt($key, $iv, $encrypted)
  {
    $iv_utf = mb_convert_encoding($iv, 'UTF-8');
    return mcrypt_decrypt($this->mcrypt_cipher, $key, base64_decode($encrypted), $this->mcrypt_mode, $iv_utf);
  }
}



$encrypted = "UmzUCnAzThH0nMkIuMisqg==";
$key = "qwertyuiopasdfghjklzxcvbnmqwerty";
$iv = "1234567890123456";

$foo = new Foo;
echo $foo->decrypt($key, $iv, $encrypted);

prints hello world

Wednesday, March 31, 2021
 
ariel
answered 7 Months ago
59

The problem is that in the CryptoJS code a password is used to derive the key and the IV to be used for AES encryption, but mcrypt only uses the key to encrypt/decrypt. This information needs to be passed to php. Since you don't want to transmit the password, you have to derive the key and IV in the same way in php.

The following code derives the key and IV from a password and salt. It is modeled after the code in my answer here (for more information).

function evpKDF($password, $salt, $keySize = 8, $ivSize = 4, $iterations = 1, $hashAlgorithm = "md5") {
    $targetKeySize = $keySize + $ivSize;
    $derivedBytes = "";
    $numberOfDerivedWords = 0;
    $block = NULL;
    $hasher = hash_init($hashAlgorithm);
    while ($numberOfDerivedWords < $targetKeySize) {
        if ($block != NULL) {
            hash_update($hasher, $block);
        }
        hash_update($hasher, $password);
        hash_update($hasher, $salt);
        $block = hash_final($hasher, TRUE);
        $hasher = hash_init($hashAlgorithm);

        // Iterations
        for ($i = 1; $i < $iterations; $i++) {
            hash_update($hasher, $block);
            $block = hash_final($hasher, TRUE);
            $hasher = hash_init($hashAlgorithm);
        }

        $derivedBytes .= substr($block, 0, min(strlen($block), ($targetKeySize - $numberOfDerivedWords) * 4));

        $numberOfDerivedWords += strlen($block)/4;
    }

    return array(
        "key" => substr($derivedBytes, 0, $keySize * 4),
        "iv"  => substr($derivedBytes, $keySize * 4, $ivSize * 4)
    );
}

The salt is generated during encryption in CryptoJS and needs to be sent to php with the ciphertext. Before invoking evpKDF the salt has to be converted to a binary string from hex.

$keyAndIV = evpKDF("Secret Passphrase", hex2bin($saltHex));
$decryptPassword = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, 
        $keyAndIV["key"], 
        hex2bin($cipherTextHex), 
        MCRYPT_MODE_CBC, 
        $keyAndIV["iv"]);

If only encryptedPassword.toString() was sent to the server, then it is necessary to split the salt and actual ciphertext before use. The format is a proprietary OpenSSL-compatible format with the first 8 bytes being "Salted__", the next 8 bytes being the random salt and the rest is the actual ciphertext. Everything together is Base64-encoded.

function decrypt($ciphertext, $password) {
    $ciphertext = base64_decode($ciphertext);
    if (substr($ciphertext, 0, 8) != "Salted__") {
        return false;
    }
    $salt = substr($ciphertext, 8, 8);
    $keyAndIV = evpKDF($password, $salt);
    $decryptPassword = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, 
            $keyAndIV["key"], 
            substr($ciphertext, 16), 
            MCRYPT_MODE_CBC, 
            $keyAndIV["iv"]);

    // unpad (PKCS#7)
    return substr($decryptPassword, 0, strlen($decryptPassword) - ord($decryptPassword[strlen($decryptPassword)-1]));
}

The same can be achieved with the OpenSSL extension instead of Mcrypt:

function decrypt($ciphertext, $password) {
    $ciphertext = base64_decode($ciphertext);
    if (substr($ciphertext, 0, 8) != "Salted__") {
        return false;
    }
    $salt = substr($ciphertext, 8, 8);
    $keyAndIV = evpKDF($password, $salt);
    $decryptPassword = openssl_decrypt(
            substr($ciphertext, 16), 
            "aes-256-cbc",
            $keyAndIV["key"], 
            OPENSSL_RAW_DATA, // base64 was already decoded
            $keyAndIV["iv"]);

    return $decryptPassword;
}
Tuesday, June 1, 2021
 
Sethunath
answered 5 Months ago
93

The main issue appears to be that your string_encrypt and string_decrypt PHP functions don't have access to the $key variable, so for the encryption key mcrypt_encrypt is using . See this question for an explanation. PHP should report a notice that key is undefined, have you turned off error reporting perhaps? Echo the key from inside the encrypt function to confirm this.

Another issue is a bug in the Mcrypt JS library. This library pads the encryption key with if the key length is less than 32 bytes, the problem is that this is not how the PHP mcrypt_encrypt function pads the key. The mcrypt_encrypt function pads the key up to the nearest valid key length (16, 24, or 32 bytes). The issue in mcrypt.js is at lines 63 and 64, change this:

if(key.length<32)
    key+=Array(33-key.length).join(String.fromCharCode(0));

to this:

if(key.length<16)
    key+=Array(17-key.length).join(String.fromCharCode(0));
else if(key.length<24 && key.length>16)
    key+=Array(25-key.length).join(String.fromCharCode(0));
else if(key.length<32 && key.length>24)
    key+=Array(33-key.length).join(String.fromCharCode(0));

Now we can confirm the fix...

PHP:

function string_encrypt($string) {
    $crypted_text = mcrypt_encrypt(MCRYPT_RIJNDAEL_256, "", $string, MCRYPT_MODE_ECB);
    return $crypted_text;
}

$test_str = "This is test message to be encrypted.";
$enc_str = string_encrypt($test_str);
echo bin2hex($enc_str);

Output:
f98fca4ddc4c10d6cd47df56b081b78566ee4facbcf2254b46f7809d9b255529d2078f28b150e802d72818be1888536fac6219f6ce7b9d9332a24afa09288f0e

Javascript:

function toHex(str) {
    var hex = '';
    for(var i=0;i<str.length;i++) {
        var val = ''+str.charCodeAt(i).toString(16);
        if(val.length == 1)
            hex += '0'+val;
        else
            hex += val;
    }
    return hex;
}

var enc_str = mcrypt.Encrypt("This is test message to be encrypted.", "", "", "rijndael-256", "ecb");
alert(toHex(enc_str));

Output:
f98fca4ddc4c10d6cd47df56b081b78566ee4facbcf2254b46f7809d9b255529d2078f28b150e802d72818be1888536fac6219f6ce7b9d9332a24afa09288f0e

Finally, all of these encryption functions produce binary as their output. Binary cannot be written as plain text in most cases without damaging the data. To solve this, either encode the binary to Hex or Base64 and then decode it before trying to decrypt.

So to get everything working...

<?php 
$key = 'testtesttesttesttesttesttesttest';

function string_encrypt($string, $key) {
    $crypted_text = mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $key, $string, MCRYPT_MODE_ECB);
    return $crypted_text;
}

function string_decrypt($encrypted_string, $key) {
    $decrypted_text = mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $key, $encrypted_string, MCRYPT_MODE_ECB);
    return trim($decrypted_text);
}

echo $test_str = 'This is test message to be encrypted.';   echo '<br />';
$enc_str = string_encrypt($test_str, $key);
echo bin2hex($enc_str);                                     echo '<br />';
echo string_decrypt($enc_str, $key);                        echo '<br />';

?>

<script src='rijndael.js'></script>
<script src='mcrypt.js'></script>

<script lang='javascript'>
    function toHex(str) {
        var hex = '';
        for(var i=0;i<str.length;i++) {
            var val = ''+str.charCodeAt(i).toString(16);
            if(val.length == 1)
                hex += '0'+val;
            else
                hex += val;
        }
        return hex;
    }
    function hexToString (hex) {
        var str = '';
        for (var i=0; i<hex.length; i+=2) {
            str += ''+String.fromCharCode(parseInt(hex.charAt(i)+hex.charAt(i+1), 16));
        }
        return str;
    }
    var enc_str = mcrypt.Encrypt('<?php echo $test_str ?>', '', 'testtesttesttesttesttesttesttest', 'rijndael-256', 'ecb');
    alert(toHex(enc_str));
    alert(mcrypt.Decrypt(hexToString('<?php echo bin2Hex($enc_str) ?>'), '', 'testtesttesttesttesttesttesttest', 'rijndael-256', 'ecb').replace(/x00+$/g, '')); 
</script>

A few more notes...

  1. You cannot trim the output of the string_encrypt function. This will cause leading or trailing zeros to be removed, which will make it so that you cannot decrypt the output.
  2. ECB mode is insecure and you really shouldn't use it. CBC is the way to go. CBC does require an IV, and the IV must be the same for both encryption and decryption.
  3. Javascript encryption is not secure for various reasons, given your usage of it anyone could simply view the pages source or debug the running javascript to get the encryption key. Read the link posted by ntoskrnl in your question comments.

Update:

Your Base64 encoding issue occurs because the library you're using doesn't work with binary data. This is a fairly common issue for Base64 javascript libraries. I'd recommend using this library instead.

As for the trailing ? characters when decrypting with javascript, you need to trim the decrypted output. You're doing this in your PHP string_decrypt method, but not in your javascript. You can trim the decrypted output by doing a regex replace on all characters at the end of the string.

Example:

mcrypt.Decrypt(dec_str,'').replace(/x00+$/g, '')

I should have included this in my original post, but I didn't notice the characters in the output because FF's alert box doesn't display them. Sorry about that.

Finally, I noticed another bug in the Mcrypt JS library. Lines 41 to 47:

var ciphers={       //  block size, key size
    "rijndael-128"  :[  16,         32],
    "rijndael-192"  :[  24,         32],
    "rijndael-256"  :[  32,         32],
    "serpent"       :[  16,         32],
    "twofish"       :[  16,         32],
}

Notice the comma at the end of the "twofish" line. Firefox and Chrome don't seem to mind this, but IE8 will report an error and fail to load the mcrypt library because of it. To fix the issue change:

"twofish"       :[  16,         32],

to:

"twofish"       :[  16,         32]
Monday, July 5, 2021
 
Octopus
answered 4 Months ago
14

Fixed it by using the following code.

$data = base64_decode(str_replace(' ', '+', $_GET['data']));

For whatever reason, PHP was converting the +'s from the GET variablesinto spaces.

Saturday, August 14, 2021
 
docaholic
answered 2 Months ago
16

Your results are the same, as far as I can see - it's just that in Java, bytes are signed. (That's icky, but it doesn't affect the actual bits you're getting.)

If you add 256 to every negative value in the Java results, you'll see they're the same as the .NET code:

.NET:      241  100  194  184  166

Java:      -15  100  -62  -72  -90

Java+256:  241  100  194  184  166
for -ve

(etc)

Alternatively, just print out the unsigned hex representation of the two byte arrays - or even base64-encode them - and you'll see they're the same.

Saturday, August 14, 2021
 
janlindso
answered 2 Months ago
Only authorized users can answer the question. Please sign in first, or register a free account.
Not the answer you're looking for? Browse other questions tagged :