Asked  7 Months ago    Answers:  5   Viewed   75 times

I have a function in PHP that encrypts text as follows:

function encrypt($text)
{
    $Key = "MyKey";

    return trim(base64_encode(mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $Key, $text, MCRYPT_MODE_ECB, mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_ECB), MCRYPT_RAND))));
}

How do I decrypt these values in Python?

 Answers

87

To decrypt this form of encryption, you will need to get a version of Rijndael. One can be found here. Then you will need to simulate the key and text padding used in the PHP Mcrypt module. They add '' to pad out the text and key to the correct size. They are using a 256 bit block size and the key size used with the key you give is 128 (it may increase if you give it a bigger key). Unfortunately, the Python implementation I've linked to only encodes a single block at a time. I've created python functions which simulate the encryption (for testing) and decryption in Python

import rijndael
import base64

KEY_SIZE = 16
BLOCK_SIZE = 32

def encrypt(key, plaintext):
    padded_key = key.ljust(KEY_SIZE, '')
    padded_text = plaintext + (BLOCK_SIZE - len(plaintext) % BLOCK_SIZE) * ''

    # could also be one of
    #if len(plaintext) % BLOCK_SIZE != 0:
    #    padded_text = plaintext.ljust((len(plaintext) / BLOCK_SIZE) + 1 * BLOCKSIZE), '')
    # -OR-
    #padded_text = plaintext.ljust((len(plaintext) + (BLOCK_SIZE - len(plaintext) % BLOCK_SIZE)), '')

    r = rijndael.rijndael(padded_key, BLOCK_SIZE)

    ciphertext = ''
    for start in range(0, len(padded_text), BLOCK_SIZE):
        ciphertext += r.encrypt(padded_text[start:start+BLOCK_SIZE])

    encoded = base64.b64encode(ciphertext)

    return encoded


def decrypt(key, encoded):
    padded_key = key.ljust(KEY_SIZE, '')

    ciphertext = base64.b64decode(encoded)

    r = rijndael.rijndael(padded_key, BLOCK_SIZE)

    padded_text = ''
    for start in range(0, len(ciphertext), BLOCK_SIZE):
        padded_text += r.decrypt(ciphertext[start:start+BLOCK_SIZE])

    plaintext = padded_text.split('x00', 1)[0]

    return plaintext

This can be used as follows:

key = 'MyKey'
text = 'test'

encoded = encrypt(key, text)
print repr(encoded)
# prints 'I+KlvwIK2e690lPLDQMMUf5kfZmdZRIexYJp1SLWRJY='

decoded = decrypt(key, encoded)
print repr(decoded)
# prints 'test'

For comparison, here is the output from PHP with the same text:

$ php -a
Interactive shell

php > $key = 'MyKey';
php > $text = 'test';
php > $output = mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $key, $text, MCRYPT_MODE_ECB);
php > $encoded = base64_encode($output);
php > echo $encoded;
I+KlvwIK2e690lPLDQMMUf5kfZmdZRIexYJp1SLWRJY=
Wednesday, March 31, 2021
 
Markol
answered 7 Months ago
94

PHP 5.6 has stronger encryption requirements than 5.4. In 5.6 you'll get this warning, which is really an error because it actually causes encryptions and decryptions to fail:

Warning: mcrypt_encrypt(): Key of size xx not supported by this algorithm. Only keys of sizes 16, 24 or 32 supported.

...where "xx" is the length of your salt value. So the salt value has to be exactly 16, 24, or 32 characters in length.

Saturday, May 29, 2021
 
JohnnyW
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
28

You can see in the padAndMcrypt() function, that the given $value is serialized using PHP's serialize() function. You can re-implement the unserialize() function in Java or you can split the byte array yourself if you're always encrypting strings in PHP.

int firstQuoteIndex = 0;
while(decValue[firstQuoteIndex] != (byte)'"') firstQuoteIndex++;
return new String(Arrays.copyOfRange(decValue, firstQuoteIndex + 1, decValue.length-2));

Full code:

public static String decrypt(byte[] keyValue, String ivValue, String encryptedData) throws Exception {
    Key key = new SecretKeySpec(keyValue, "AES");
    byte[] iv = Base64.decode(ivValue.getBytes("UTF-8"), Base64.DEFAULT);
    byte[] decodedValue = Base64.decode(encryptedData.getBytes("UTF-8"), Base64.DEFAULT);

    Cipher c = Cipher.getInstance("AES/CBC/PKCS7Padding"); // or PKCS5Padding
    c.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
    byte[] decValue = c.doFinal(decodedValue);

    int firstQuoteIndex = 0;
    while(decValue[firstQuoteIndex] != (byte)'"') firstQuoteIndex++;
    return new String(Arrays.copyOfRange(decValue, firstQuoteIndex + 1, decValue.length-2));
}

Verifying the MAC is always a good idea, because it prevents some attacks such as the padding oracle attack. It is also a very good way to detect general modifications of ciphertexts.

Full code with MAC verification:

public static String decrypt(byte[] keyValue, String ivValue, String encryptedData, String macValue) throws Exception {
    Key key = new SecretKeySpec(keyValue, "AES");
    byte[] iv = Base64.decode(ivValue.getBytes("UTF-8"), Base64.DEFAULT);
    byte[] decodedValue = Base64.decode(encryptedData.getBytes("UTF-8"), Base64.DEFAULT);

    SecretKeySpec macKey = new SecretKeySpec(keyValue, "HmacSHA256");
    Mac hmacSha256 = Mac.getInstance("HmacSHA256");
    hmacSha256.init(macKey);
    hmacSha256.update(ivValue.getBytes("UTF-8"));
    byte[] calcMac = hmacSha256.doFinal(encryptedData.getBytes("UTF-8"));
    byte[] mac = Hex.decodeHex(macValue.toCharArray());
    if (!secureEquals(calcMac, mac))
        return null; // or throw exception

    Cipher c = Cipher.getInstance("AES/CBC/PKCS7Padding"); // or PKCS5Padding
    c.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
    byte[] decValue = c.doFinal(decodedValue);

    int firstQuoteIndex = 0;
    while(decValue[firstQuoteIndex] != (byte)'"') firstQuoteIndex++;
    return new String(Arrays.copyOfRange(decValue, firstQuoteIndex + 1, decValue.length-2));
}

/* Constant-time compare to prevent timing attacks on invalid authentication tags. */
public static boolean secureEquals(final byte[] known, final byte[] user) {
    int knownLen = known.length;
    int userLen = user.length;

    int result = knownLen ^ userLen;
    for (int i = 0; i < knownLen; i++) {
        result |= known[i] ^ user[i % userLen];
    }
    return result == 0;
}
Tuesday, August 10, 2021
 
McAn
answered 3 Months ago
90

The string is encoded to bytes using the UTF-16 encoding. The first two bytes are a BOM. Then each character is encoded to two bytes.

From the documentation for Encoding.Unicode:

Gets an encoding for the UTF-16 format using the little endian byte order.

To get the original string you need to decode it back from UTF-16 bytes to a Unicode string.

print aes.decrypt(text).decode('utf-16')
Sunday, September 12, 2021
 
Statsar
answered 1 Month 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 :