Aes Python Encryption and Ruby Encryption - Different Behaviour

AES Python encryption and Ruby encryption - different behaviour?

This is understandably confusing—PyCrypto has gone a bit off the rails here and broken with the usual implementation. If you're familiar enough with what encrypted data should normally look like, the Python output looks blatantly wrong and gives you a place to start. If you're not, it's easy to wonder what the heck went wrong and have no idea where to start looking.

In a "normal" implementation, padding will be used by default and you'll end up (in this case) with encrypted output that's 16 bytes longer.

Encrypted using Ruby, for example, this is the result:

>> ciphertext
=> "\xD6\x83\x8Dd!VT\x92\xAA`A\x05\xE0\x9B\x8B\xF1\xD5f\xC7\xFFNI\xC7N\xBC-;!\f\xF1!\xB4"
>> ciphertext.bytes
=> [214, 131, 141, 100, 33, 86, 84, 146, 170, 96, 65, 5, 224, 155, 139, 241, 213, 102, 199, 255, 78, 73, 199, 78, 188, 45, 59, 33, 12, 241, 33, 180]

PyCrypto, for reasons I cannot immediately find, has chosen to work only with unpadded data. When interchanging data with PyCrypto, you'll want to configure any other libraries appropriately.

In the case of Ruby's OpenSSL library, the Cipher object exposes a padding property which can be used to disable padding:

>> require 'openssl'
=> true
>> obj2 = OpenSSL::Cipher::Cipher.new("AES-128-CBC")
=> #<OpenSSL::Cipher::Cipher:0x007fe62407a9b0>
>> obj2.decrypt
=> #<OpenSSL::Cipher::Cipher:0x007fe62407a9b0>
>> obj2.key = 'This is a key123'
=> "This is a key123"
>> obj2.iv = 'This is an IV456'
=> "This is an IV456"
>> obj2.padding = 0
=> 0
>> ciphertext = [214, 131, 141, 100, 33, 86, 84, 146, 170, 96, 65, 5, 224, 155, 139, 241].pack('c*')
=> "\xD6\x83\x8Dd!VT\x92\xAA`A\x05\xE0\x9B\x8B\xF1"
>> obj2.update(ciphertext) + obj2.final
=> "The answer is no"

AES encryption/decryption between Ruby-OpenSSL, PyCrypto

OpenSSL applies PKCS#5Padding by default, so this is also used automatically when encrypting data with OpenSSL::Cipher in AES-CBC mode (cf. OpenSSL docs). So there's no need to perform manual padding when using Ruby.

The padding has to be done manually in Python when using PyCrypto.

Once you apply this padding scheme in Python, both encrypted Base64 strings should match.

Decrypt Data in Ruby using OpenSSL

I have found OpenSSL integration to be lacking in Ruby, and generally more cumbersome (uglier api / less convention over configuration) for cryptographic work then mcrypt.

If you're using mcrypt in python successfully, why not use mcrypt in ruby too? https://github.com/kingpong/ruby-mcrypt

Something in the lines of:

require 'rubygems'
require 'mcrypt'
crypto = Mcrypt.new("rijndael-128", :cbc, key, iv)
decrypted_data = crypto.decrypt(encrypted_data)

should work in your case.

I can extend on this answer later today or tomorrow, got some traveling to do first

How to choose an AES encryption mode (CBC ECB CTR OCB CFB)?

  • ECB should not be used if encrypting more than one block of data with the same key.

  • CBC, OFB and CFB are similar, however OFB/CFB is better because you only need encryption and not decryption, which can save code space.

  • CTR is used if you want good parallelization (ie. speed), instead of CBC/OFB/CFB.

  • XTS mode is the most common if you are encoding a random accessible data (like a hard disk or RAM).

  • OCB is by far the best mode, as it allows encryption and authentication in a single pass. However there are patents on it in USA.

The only thing you really have to know is that ECB is not to be used unless you are only encrypting 1 block. XTS should be used if you are encrypting randomly accessed data and not a stream.

  • You should ALWAYS use unique IV's every time you encrypt, and they should be random. If you cannot guarantee they are random, use OCB as it only requires a nonce, not an IV, and there is a distinct difference. A nonce does not drop security if people can guess the next one, an IV can cause this problem.

AES CTR decryption: Cryptography and Cryptodome give different results

In PyCryptodome the counter is started at 1 by default. Also, the counter for little endian counts as follows: 0x0100...0000, 0x0200...0000, 0x0300...0000 etc.

Since in Cryptography the endianess of the counter cannot be configured and big endian is used, this count cannot be implemented. Although the start value can be explicitly set to 0x0100...00 , but the counter would then count: 0x0100...0000, 0x0100...0001, 0x0100...0002 etc.

This can be verified with the following code:

from Crypto.Cipher import AES
from Crypto.Util import Counter
key = b'\x12' * 32
decryptor = AES.new(key, AES.MODE_CTR,counter=Counter.new(nbits=128, little_endian=True, initial_value=1))
print(decryptor.decrypt(b'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx').hex())

from cryptography.hazmat.primitives.ciphers import Cipher, modes, algorithms
key = b'\x12' * 32
decryptor = Cipher(algorithms.AES(key), modes.CTR(b'\x01' + b'\0' * 15)).decryptor()
print(decryptor.update(b'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx').hex())

with the output:

faebe2ab213094fcd5c9ec3dae32372b 13b3b971b7694faa5e15f5387ac5a67f bc5dbc82ce54cf1bbe2719488b322078
faebe2ab213094fcd5c9ec3dae32372b 6ddda72218780c287bc74956395bf7db 0603820b26889ec64e7f7964a14518c5

Because the counter value for the first block matches, the first block is identical. However, because of the different values for the following blocks the following blocks are different.

I currently don't see any way how the Cryptography library could be used to generate the PyCryptodome library result when PyCryptodome is configured for little endian.



Related Topics



Leave a reply



Submit