14

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?

dharmesh
  • 245
  • 1
  • 2
  • 6
  • nobody can decrypt this, you throw away the iv. –  Nov 21 '11 at 20:16
  • 2
    @hop - ECB mode can be decrypted without the IV. The IV is not used in ECB mode for encryption or decryption. – wkl Nov 21 '11 at 20:20
  • 1
    @birryree: right! i only saw that the iv argument was set and assumed… this shows why you don't write your whole programm in one line. –  Nov 21 '11 at 21:04
  • @dharmesh - I am attempting to solve your problem, but thanks to PHP, the `MCRYPT_RIJNDAEL_256` algorithm is not the same as `AES 256` (more rounds are done in Rijndael). I have to find a Python implementation or pure Rijndael 256, which it doesn't seem like PyCrypto nor M2Crypto support. – wkl Nov 21 '11 at 21:49
  • @dharmesh: why don't you use something common on the php side in the first place? –  Nov 21 '11 at 22:45
  • That would be a good option too - Rijndael-256 is not as widely used, and if you want to use AES, you would be using Rijndael-128 (`MCRYPT_RIJNDAEL_128`). PHP's versions of Rijndael 192 and 256 are not AES-compatible. dharmesh - the simplest way would be to decrypt and re-encrypt your existing data with `MCRYPT_RIJNDAEL_128`, which would allow you to use popular Python libraries to decrypt your data. – wkl Nov 21 '11 at 22:55
  • Are you using Python 2.x or 3.x? – 101100 Nov 22 '11 at 17:03

3 Answers3

18

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 '\0' 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, '\0')
    padded_text = plaintext + (BLOCK_SIZE - len(plaintext) % BLOCK_SIZE) * '\0'

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

    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, '\0')

    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=
101100
  • 2,666
  • 23
  • 28
  • 1
    Absolutely brilliant! I was able to get your test code running. Then, I ran the decrypt function (in Python) against encrypted values from PHP. Works like a charm. Thanks a bunch. – dharmesh Nov 22 '11 at 21:59
  • apart from the key len > block size part you should use string.ljust() to pad the key. –  Nov 23 '11 at 17:36
  • @hop Thanks for the heads up on string.ljust(). I've updated the key padding to use that. The text padding still seems better the other way. – 101100 Nov 24 '11 at 17:05
  • What is required to adapt this script to Python 3? – gavinmh Dec 06 '14 at 15:28
  • 2
    @101100 Installed package using "pip install rijndael" but giving error. r = rijndael.rijndael(padded_key, BLOCK_SIZE) AttributeError: 'module' object has no attribute 'rijndael' – iit2011081 Jan 27 '18 at 17:48
  • 1
    @101100 I have try this code but its give me attribute error "AttributeError: 'module' object has no attribute 'rijndael'" – Harshit Trivedi May 29 '18 at 06:36
4

If you're willing to use MCRYPT_RIJNDAEL_128 rather than 256 on the PHP side, this is as simple as:

from Crypto.Cipher import AES
import base64
key="MyKey"

def decrypt(text)
    cipher=AES.new(key)
    return cipher.decrypt(base64.b64decode(text))
Adam McKenna
  • 583
  • 4
  • 2
2

Although the answer from @101100 was a good one at the time, it's no longer viable. The reference is now a broken link, and the code would only run on older Pythons (<3).

Instead, the pprp project seems to fill the void nicely. On Python 2 or Python 3, just pip install pprp, then:

import pprp
import base64

KEY_SIZE = 16
BLOCK_SIZE = 32


def encrypt(key, plaintext):
    key = key.encode('ascii')
    plaintext = plaintext.encode('utf-8')
    padded_key = key.ljust(KEY_SIZE, b'\0')

    sg = pprp.data_source_gen(plaintext, block_size=BLOCK_SIZE)
    eg = pprp.rjindael_encrypt_gen(padded_key, sg, block_size=BLOCK_SIZE)

    ciphertext = pprp.encrypt_sink(eg)

    encoded = base64.b64encode(ciphertext)

    return encoded.decode('ascii')


def decrypt(key, encoded):
    key = key.encode('ascii')
    padded_key = key.ljust(KEY_SIZE, b'\0')

    ciphertext = base64.b64decode(encoded.encode('ascii'))

    sg = pprp.data_source_gen(ciphertext, block_size=BLOCK_SIZE)
    dg = pprp.rjindael_decrypt_gen(padded_key, sg, block_size=BLOCK_SIZE)

    return pprp.decrypt_sink(dg).decode('utf-8')


key = 'MyKey'
text = 'test'

encoded = encrypt(key, text)
print(repr(encoded))
# prints 'ju0pt5Y63Vj4qiViL4VL83Wjgirq4QsGDkj+tDcNcrw='

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

I'm a little dismayed that the ciphertext comes out different than what you see with 101100's answer. I have, however, used this technique to successfully decrypt data encrypted in PHP as described in the OP.

Jason R. Coombs
  • 41,115
  • 10
  • 83
  • 93