Search code examples
pythonencryptioncryptographyaespycrypto

How AES in CTR works for Python with PyCrypto?


I am using python 2.7.1 I want to encrypt sth using AES in CTR mode. I installed PyCrypto library for python. I wrote the following code:

secret = os.urandom(16)
crypto = AES.new(os.urandom(32), AES.MODE_CTR, counter=lambda: secret)
encrypted = crypto.encrypt("asdk")
print crypto.decrypt(encrypted)

i have to run crypto.decrypt as many times as the byte size of my plaintext in order to get correctly the decrypted data. I.e:

encrypted = crypto.encrypt("test")
print crypto.decrypt(encrypted)
print crypto.decrypt(encrypted)
print crypto.decrypt(encrypted)
print crypto.decrypt(encrypted)

The last call to decrypt will give me the plaintext back. The other outputs from decrypt are some gibberish strings . I am wondering if this is normal or not? Do i have to include into a loop with size equal of my plaintext every time or i have gotten sth wrong?


Solution

  • According to @gertvdijk, AES_CTR is a stream cipher which does not need padding. So I've deleted the related codes.

    Here's something I know.

    1. You have to use a same key(the first parameter in AES.new(...)) in encryption and decryption, and keep the key private.

    2. The encryption/decryption methods are stateful, that means crypto.en(de)crypt("abcd")==crypto.en(de)crypt("abcd") is not always true. In your CTR, your counter callback always returns a same thing, so it becomes stateless when encrypt (I am not 100% sure it is the reason), but we still find that it is somewhat stateful in decryption. As a conclusion, we should always use a new object to do them.

    3. The counter callback function in both encryption and decryption should behave the same. In your case, it is to make both of them return the same secret. Yet I don't think the secret is a "secret". You can use a random generated "secret" and pass it across the communicating peers without any encryption so that the other side can directly use it, as long as the secret is not predictable.

    So I would write my cipher like this, hope it will offer some help.

    import os
    import hashlib
    import Crypto.Cipher.AES as AES
    
    class Cipher:
    
            @staticmethod
            def md5sum( raw ):
                    m = hashlib.md5()
                    m.update(raw)
                    return m.hexdigest()
    
            BS = AES.block_size
    
            @staticmethod 
            def pad( s ):
                    """note that the padding is no necessary"""
                    """return s + (Cipher.BS - len(s) % Cipher.BS) * chr(Cipher.BS - len(s) % Cipher.BS)"""
                    return s
    
            @staticmethod
            def unpad( s ):
                    """return s[0:-ord(s[-1])]"""
                    return s
    
            def __init__(self, key):
                    self.key = Cipher.md5sum(key)
                    #the state of the counter callback 
                    self.cnter_cb_called = 0 
                    self.secret = None
    
            def _reset_counter_callback_state( self, secret ):
                    self.cnter_cb_called = 0
                    self.secret = secret
    
            def _counter_callback( self ):
                    """
                    this function should be stateful
                    """
                    self.cnter_cb_called += 1
                    return self.secret[self.cnter_cb_called % Cipher.BS] * Cipher.BS
    
    
            def encrypt(self, raw):
                    secret = os.urandom( Cipher.BS ) #random choose a "secret" which is not secret
                    self._reset_counter_callback_state( secret )
                    cipher = AES.new( self.key, AES.MODE_CTR, counter = self._counter_callback )
                    raw_padded = Cipher.pad( raw )
                    enc_padded = cipher.encrypt( raw_padded )
                    return secret+enc_padded #yes, it is not secret
    
            def decrypt(self, enc):
                    secret = enc[:Cipher.BS]
                    self._reset_counter_callback_state( secret )
                    cipher = AES.new( self.key, AES.MODE_CTR, counter = self._counter_callback )
                    enc_padded = enc[Cipher.BS:] #we didn't encrypt the secret, so don't decrypt it
                    raw_padded = cipher.decrypt( enc_padded )
                    return Cipher.unpad( raw_padded )
    

    Some test:

    >>> from Cipher import Cipher
    >>> x = Cipher("this is key")
    >>> "a"==x.decrypt(x.encrypt("a"))
    True
    >>> "b"==x.decrypt(x.encrypt("b"))
    True
    >>> "c"==x.decrypt(x.encrypt("c"))
    True
    >>> x.encrypt("a")==x.encrypt("a")
    False #though the input is same, the outputs are different
    

    Reference: http://packages.python.org/pycrypto/Crypto.Cipher.blockalgo-module.html#MODE_CTR